Compare commits

..

160 Commits

Author SHA1 Message Date
renovate[bot] 3e0c1dd574 chore(deps): Update non-major dependencies (#1653) 2026-06-08 17:24:42 +08:00
renovate[bot] c06f7a3629 chore(deps): Lock file maintenance (#1652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-08 04:59:06 +00:00
github-actions[bot] 14790a1551 New Crowdin translations by GitHub Action (#1650)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-06-08 02:30:11 +08:00
h2zi 21593ca29d chore(l10n): update pot files 2026-06-07 18:20:25 +00:00
耗子 1a8bd4e400 chore: go mod tidy 2026-06-08 02:18:54 +08:00
耗子 865f12c75b feat: 支持计划任务备份clickhouse和redis 2026-06-08 02:17:09 +08:00
耗子 8c28ac19ec feat: 定时备份任务支持 ClickHouse/Redis/Valkey
cron 备份脚本生成在 mysql/postgresql 之外扩展 clickhouse/redis/valkey;
前端计划任务表单新增对应备份类型(按安装状态显示),Redis/Valkey 整实例
无库名,选中后自动以实例名作为备份目标。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 01:45:09 +08:00
github-actions[bot] ee15bfa67c New Crowdin translations by GitHub Action (#1649)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-06-08 01:33:12 +08:00
h2zi c04fdfc7aa chore(l10n): update pot files 2026-06-07 17:21:48 +00:00
耗子 ded7909fdb refactor: 重写 apache/nginx 配置解析器为 AST 驱动
apache 包废弃脆弱的字符级 lexer,重写为三层单遍解析器(逻辑行合并续行 +
shell 分词 + 递归下降)+ 统一有序 AST(VirtualHost 并入 Block、参数分离
引号信息)。修复嵌套块被静默丢弃、不支持续行符、特殊字符粘连、order 表
VirtualHost 排序等缺陷;proxy/redirect/vhost 读写全部改为 AST 驱动,删除
所有正则反解析与脆弱的 Sscanf/strings.Index。

nginx 包片段读端(proxy/redirect/upstream)从正则反解析改为 gonginx AST 遍历。

types.Vhost 接口清理:用 ConfigScope 类型常量替代 stringly-typed 的
"site"/"shared",用 SetRawConfig 独立方法替代 skipComment 可变布尔参数。

新增 robustness_test.go:用 apache 官方及社区真实配置验证语义无损、幂等、边界容错。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 00:00:48 +08:00
耗子 b5083dd8fa feat: 优化s3 sdk 2026-06-07 21:47:15 +08:00
耗子 c7e078b7f8 Merge remote-tracking branch 'origin/main' 2026-06-07 21:21:33 +08:00
耗子 3a937db506 fix: ipv6证书问题 2026-06-07 21:21:28 +08:00
renovate[bot] f92c661f33 chore(deps): Update non-major dependencies to v1.52.0 (#1648)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-06 21:02:41 +00:00
renovate[bot] 8c29bba051 chore(deps): Update non-major dependencies to v24.13.1 (#1647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-06 04:43:21 +00:00
耗子 40e6f8e08f refactor: 重写s3 sdk 2026-06-05 23:33:35 +08:00
耗子 7d0a6ced8c Merge remote-tracking branch 'origin/main' 2026-06-05 19:54:50 +08:00
耗子 104cbc00f1 chore: run go mod tidy 2026-06-05 19:54:45 +08:00
h2zi 75c4f44262 chore: update mocks 2026-06-05 11:43:55 +00:00
耗子 ef44ab8f0d Merge remote-tracking branch 'origin/main' 2026-06-05 19:42:59 +08:00
耗子 568ce6b270 feat: 新增容器无极日志 2026-06-05 19:42:53 +08:00
renovate[bot] 20df3c5f81 chore(deps): Update non-major dependencies to v11.5.2 (#1646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-05 18:55:46 +08:00
耗子 8e3999ccbe feat: 新增redis/valkey备份 2026-06-05 15:51:12 +08:00
耗子 b4efbe823f Merge remote-tracking branch 'origin/main' 2026-06-05 14:26:15 +08:00
耗子 ca218b31ec feat: 新增clickhouse复制 2026-06-05 14:26:09 +08:00
renovate[bot] e1823854bd chore(deps): Update non-major dependencies (#1645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-04 17:56:05 +00:00
renovate[bot] 28bd1809cc chore(deps): Update non-major dependencies (#1644)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-04 00:43:01 +00:00
耗子 120baf3bd7 fix: 修复清理日志成功提示显示异常
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 04:22:59 +08:00
耗子 cbabd963df fix: 获取备份路径时自动创建目录
修复上传 MySQL 等备份文件时,因备份目录(如 /backup/mysql/)不存在导致
报错"文件不存在"的问题。将创建目录逻辑收敛到 GetDefaultPath,所有调用方均受益。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 04:05:51 +08:00
renovate[bot] 6a4f91e6cc chore(deps): Update pnpm to v11.5.1 (#1643)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-02 20:15:01 +08:00
renovate[bot] 8792d9b5cf chore(deps): Update non-major dependencies (#1642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-02 03:40:34 +00:00
耗子 e667923c51 Merge remote-tracking branch 'origin/main' 2026-06-01 18:25:58 +08:00
耗子 c2d01c4371 fix: 修复问题 2026-06-01 18:25:52 +08:00
renovate[bot] 1b713c8f32 chore(deps): Update module github.com/shirou/gopsutil/v4 to v4.26.5 (#1640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-01 16:25:06 +08:00
renovate[bot] 561db6a861 chore(deps): Update non-major dependencies (#1639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-31 22:25:26 +00:00
耗子 9f3f0c8876 fix: 修复日志残留僵尸进程的问题 2026-05-31 22:43:25 +08:00
耗子 a06af8cd12 fix: 修复日志残留僵尸进程的问题 2026-05-31 22:31:26 +08:00
renovate[bot] a2c2797942 chore(deps): Update non-major dependencies (#1638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-30 01:09:25 +00:00
renovate[bot] c39f3e29b8 chore(deps): Update non-major dependencies (#1637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-30 00:34:07 +08:00
renovate[bot] 19f3d80717 chore(deps): Update non-major dependencies (#1635)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-27 18:47:53 +00:00
h2zi b5bdcaded6 chore: update mocks 2026-05-27 15:56:25 +00:00
fish 419cf50a37 Fix/postgresql port sync (#1636)
* refactor(postgresql): 优化配置处理并改进端口管理

- 引入 `configPath()` 方法,集中获取配置文件路径。
- 新增 `getPort()` 方法,用于从配置中动态获取 PostgreSQL 端口。
- 更新方法以使用新的配置处理机制,确保对 PostgreSQL 配置文件的访问一致性。
- 增强了配置更新期间端口变更时的错误处理和服务重启逻辑。

* fix(home): 修复数据库统计接口使用固定端口连接数据库的问题

- 更新了 HomeService,使其包含一个新的 databaseServerRepo,用于管理数据库服务器配置。
- 修改了 NewHomeService 构造函数,使其接受新的 databaseServerRepo 参数。
- 重构了数据库连接逻辑,使其利用数据库服务器存储库进行 PostgreSQL 连接。

* fix: optimize code

---------

Co-authored-by: 耗子 <haozi@loli.email>
2026-05-27 23:55:25 +08:00
耗子 b8311af661 Merge remote-tracking branch 'origin/main' 2026-05-27 21:33:45 +08:00
耗子 c8805779ed fix: optimize code 2026-05-27 21:33:39 +08:00
renovate[bot] 5a156a84cb chore(deps): Update module github.com/DeRuina/timberjack to v1.4.3 (#1634)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-27 09:14:00 +00:00
renovate[bot] 10415d3ac9 chore(deps): Update non-major dependencies (#1633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-27 02:25:56 +00:00
耗子 16025c080b fix: 过滤非备份文件 2026-05-26 03:07:20 +08:00
耗子 f7fcd0ca8a fix: build 2026-05-26 02:39:49 +08:00
耗子 220d86348c fix: lint 2026-05-26 02:37:58 +08:00
耗子 215a8cf4d6 fix: build 2026-05-26 02:34:02 +08:00
耗子 3df31d8e7f fix: build 2026-05-26 02:24:19 +08:00
renovate[bot] 430974254e chore(deps): Lock file maintenance (#1631)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-26 02:12:52 +08:00
耗子 5884d1aeed Merge remote-tracking branch 'origin/main' 2026-05-26 02:10:06 +08:00
耗子 61769323a6 fix: web 2026-05-26 02:10:00 +08:00
renovate[bot] 3e5779bd1c chore(deps): Update non-major dependencies (#1632)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-26 02:08:43 +08:00
renovate[bot] d70688503b chore(deps): Update non-major dependencies (#1629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: 耗子 <haozi@loli.email>
2026-05-26 02:02:36 +08:00
耗子 85cd8ef2bb fix: lint 2026-05-26 02:01:31 +08:00
renovate[bot] 5fd4bf2270 chore(deps): Update major dependencies to v9 (#1630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-26 01:58:24 +08:00
耗子 0a162cbf57 Merge remote-tracking branch 'origin/main' 2026-05-26 01:55:21 +08:00
耗子 a19bf8f4bf fix: web 2026-05-26 01:55:15 +08:00
github-actions[bot] 03127e7f2a New Crowdin translations by GitHub Action (#1628)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-26 01:24:37 +08:00
h2zi 1a58daa007 chore(l10n): update pot files 2026-05-25 17:23:04 +00:00
耗子 bac3dcc86d fix: openresty服务名写错了 2026-05-26 01:15:27 +08:00
耗子 4739f6ea78 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	go.mod
2026-05-26 01:10:40 +08:00
耗子 5077e087d5 chore: 更新依赖 2026-05-26 01:09:43 +08:00
耗子 ef9789a6b2 feat: 安全加固 2026-05-26 00:21:08 +08:00
renovate[bot] 24af0f21b1 chore(deps): Update module github.com/go-chi/chi/v5 to v5.3.0 (#1627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 23:52:15 +08:00
renovate[bot] f115e6f93c chore(deps): Update module github.com/go-webauthn/webauthn to v0.17.4 (#1626)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 23:39:53 +08:00
renovate[bot] 4b8779bfb4 chore(deps): Lock file maintenance (#1625)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 12:39:52 +00:00
renovate[bot] 4407d4aec2 chore(deps): Update non-major dependencies (#1622)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-25 09:36:07 +00:00
耗子 5ef6074870 fix: 网站/数据库/项目删除及应用卸载恢复 5 秒倒计时
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 02:52:09 +08:00
耗子 2427e90591 fix: 列表操作按钮使用 NFlex 统一间距避免重叠
Closes #1610

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 02:44:18 +08:00
耗子 292f22a1c2 feat: 压缩解压及面板备份支持 tar.zst 格式
Closes #1619

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 02:35:10 +08:00
renovate[bot] 7439cf489e chore(deps): Update non-major dependencies (#1621)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-21 20:49:47 +00:00
renovate[bot] 471570db77 chore(deps): Update dependency tsx to v4.22.1 (#1620)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-20 11:15:50 +00:00
renovate[bot] bd24e6de20 chore(deps): Update dependency oxfmt to ^0.50.0 (#1618)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-19 00:57:45 +00:00
renovate[bot] c81c13be1a chore(deps): Lock file maintenance (#1617)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 04:34:08 +00:00
renovate[bot] 0bb3aea1df chore(deps): Update non-major dependencies (#1616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-17 20:47:36 +00:00
renovate[bot] 8f144e3358 chore(deps): Update non-major dependencies (#1615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-16 20:35:10 +00:00
renovate[bot] fc74228e7e chore(deps): Update pnpm to v11.1.1 (#1614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 17:01:17 +00:00
renovate[bot] d1eec7f7d6 chore(deps): Update non-major dependencies (#1613)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-15 05:38:03 +00:00
renovate[bot] 05e6a10a4e chore(deps): Update dependency vite to v8.0.12 (#1612)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-14 21:07:42 +08:00
耗子 db7e342a23 feat: 禁止删除 acepanel-network 内置网络
acepanel-network 由面板自身使用,误删会导致容器编排异常。后端
Remove 拦截该网络的删除请求,前端批量删除时一并将其排除。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:39:01 +08:00
耗子 5495a17748 fix: 修复裸域名与泛域名共用 ACME TXT 记录时相互覆盖
abc.com 与 *.abc.com 共用 _acme-challenge.abc.com,需同时存在两条
TXT 记录,但 SetRecords 以 (name, type) 为单位覆盖,导致后写入的
challenge 覆盖前者,DNS 验证一直卡死。改为按记录名累积所有 keyAuth
一次性写入。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:39:01 +08:00
renovate[bot] d152f4c59f chore(deps): Update module modernc.org/sqlite to v1.50.1 (#1611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-14 01:01:33 +00:00
renovate[bot] 823635c4d8 chore(deps): Update module github.com/urfave/cli/v3 to v3.9.0 (#1609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-13 01:47:51 +00:00
renovate[bot] aa6c1fb47e chore(deps): Update module github.com/go-webauthn/webauthn to v0.17.3 (#1608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-12 18:51:32 +00:00
renovate[bot] bcff04bc36 chore(deps): Update non-major dependencies (#1606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-12 04:35:33 +00:00
renovate[bot] 8aaf4e5c78 chore(deps): Update module github.com/libtnb/cron to v0.2.1 (#1605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 21:08:51 +08:00
renovate[bot] c47ba0eac9 chore(deps): Update dependency vite-plugin-vue-devtools to v8.1.2 (#1603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 09:55:43 +00:00
renovate[bot] 9db21de85b chore(deps): Lock file maintenance (#1604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-11 05:33:46 +00:00
renovate[bot] a0a98d7c4e chore(deps): Update pnpm to v11.0.8 (#1602)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-10 08:54:33 +00:00
renovate[bot] ae6a43a780 chore(deps): Update pnpm to v11.0.7 (#1601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-10 15:18:36 +08:00
renovate[bot] 6e87b0f1b3 chore(deps): Update non-major dependencies (#1600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 17:32:24 +00:00
耗子 e76b2ee7a9 fix: 文件管理右键删除增加二次确认
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:43:29 +08:00
耗子 37a883d0bc fix: 移除删除确认默认 5 秒倒计时
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:40:10 +08:00
renovate[bot] ac48313948 chore(deps): Update non-major dependencies (#1599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-09 01:46:56 +00:00
耗子 37c6fadecf fix: 备份格式默认值错误 2026-05-08 21:26:23 +08:00
github-actions[bot] 122c538811 New Crowdin translations by GitHub Action (#1598)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 20:16:30 +08:00
h2zi 4d1bb40d26 chore(l10n): update pot files 2026-05-08 11:50:58 +00:00
耗子 f3e61dfc05 fix: lint 2026-05-08 19:49:36 +08:00
耗子 58186508bb feat: 前端现代化改造3 2026-05-08 19:26:32 +08:00
耗子 c41dab89f8 refactor: 替换 robfig/cron/v3 为 libtnb/cron
robfig/cron 已停止维护,迁移到 libtnb/cron(零三方依赖,API 设计更现代)

主要变更:
- pkg/cron/logger.go 删除,libtnb/cron 直接吃 *slog.Logger
- bootstrap.NewCron 用 wrap.Recover() / wrap.SkipIfRunning() 替代旧 cron.Recover / SkipIfStillRunning
- app.Ace.Run 处理 c.Start() error 返回值;c.Stop(ctx) 接受 context
- 6 个 job (Monitoring/FirewallScan/CertRenew/PanelTask/WebsiteStat/WebsiteExpire) 的 Run 签名从 () 改为 (context.Context) error
- jobs.Register 用 c.Add 替代 c.AddJob
- 重新生成 wire 代码

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:55:39 +08:00
耗子 2e049da24e fix: 登录页面去掉卡片 2026-05-08 18:47:37 +08:00
耗子 f6f22ecaf6 fix: zip 解压改用 7z 处理中文文件名
unzip 默认按 UTF-8 解析文件名,Windows GBK/CP936 编码的 zip 包会被转义成 #Uxxxx 形式
改用 7z x 替代,自动检测文件名编码

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:37:02 +08:00
耗子 2328456a81 fix: 修复 errcheck lint 报错
defer f.Close() 改为显式忽略返回值,跟其他地方保持一致

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 15:10:06 +08:00
耗子 f0bee01fd7 Merge remote-tracking branch 'origin/renovate/non-major-dependencies' 2026-05-08 15:07:05 +08:00
github-actions[bot] 4d6ac8371d New Crowdin translations by GitHub Action (#1597)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-08 14:57:20 +08:00
h2zi e5444f13d4 chore(l10n): update pot files 2026-05-07 21:32:48 +00:00
renovate[bot] ba53855292 chore(deps): Update pnpm to v11.0.5 2026-05-07 21:31:47 +00:00
renovate[bot] ab869d6b12 chore(deps): Update pnpm to v11.0.5 (#1596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-08 05:31:18 +08:00
耗子 2d7fbeb370 refactor: 前端 UI 全量重构与日志查看器重写
- 引入设计系统 (tokens / naive-overrides / variables.scss),收敛颜色/圆角/阴影/字号/动效到唯一真相源,Naive UI 主色改回浅绿
- 新增系统组件 (PageContainer / ConfirmDialog / FormModal / FormDrawer 等),替换并删除 AppPage / CommonPage / DeleteConfirm
- 全量视图迁移到新组件:列表页用 NFlex 排版操作列,搜索框用 !w-XX 覆盖 NInput 默认 100% 宽
- 工具箱 SSH/日志清理/磁盘 重写为更紧凑的卡片+sub-tabs 布局
- 终端页重做:macOS 风格 shell + 紧凑 tab + 红绿状态点 + 主机 drawer
- 日志查看器重写:后端新增 GET /file/tail (反向分页) + WS /follow,前端用 anser 渲染 ANSI 颜色 + 原生 div 滚动 + WS 自动重连
- 修复多处 NPagination page-count 与 item-count 同时设置的警告
- 网站列表支持单击网站名打开编辑

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 05:30:20 +08:00
renovate[bot] 95790da784 chore(deps): Update pnpm to v11.0.4 (#1595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-08 00:45:59 +08:00
renovate[bot] 2ffd099c8a chore(deps): Update pnpm to v11 (#1594)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-08 00:42:05 +08:00
耗子 f52d324073 chore(deps): 升级 vite 到 ^8.0.11
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:44:45 +08:00
耗子 787d489cfc chore: 适配 pnpm v11 的 allowBuilds 配置
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 20:44:20 +08:00
renovate[bot] 407148dfe3 chore(deps): Update non-major dependencies (#1593)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 20:28:41 +08:00
github-actions[bot] 6b1eaa1aaa New Crowdin translations by GitHub Action (#1592)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-05-07 05:16:38 +08:00
h2zi 249424332e chore(l10n): update pot files 2026-05-06 21:16:10 +00:00
耗子 68efc98c06 fix: 用户列表与文件内联输入框未占满列宽 2026-05-07 05:13:08 +08:00
h2zi aab5029848 chore(l10n): update pot files 2026-05-06 21:01:56 +00:00
耗子 b7041f9253 feat: 进程列表左键点击弹出操作菜单 2026-05-07 05:00:18 +08:00
耗子 a5a6409be0 fix: 清除日志后实时日志未刷新 2026-05-07 04:56:01 +08:00
耗子 3eb2696690 fix: environment列表无数据时返回null 2026-05-07 04:55:57 +08:00
耗子 dbcbbf9d95 feat: 备份支持指定压缩文件格式,close #1582 2026-05-07 04:31:09 +08:00
renovate[bot] 2c078d90d7 chore(deps): Update module github.com/go-webauthn/webauthn to v0.17.2 (#1591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 16:37:08 +00:00
耗子 f08136242e feat: 已安装应用列表显示运行状态 2026-05-06 18:05:19 +08:00
耗子 d503d9f083 fix: docker compose在目录大小情况下获取状态失败,close #1590 2026-05-06 03:14:46 +08:00
renovate[bot] 3147f70dde chore(deps): Update module github.com/gookit/color to v1.6.1 (#1589)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 21:57:48 +00:00
renovate[bot] e2815a430e chore(deps): Lock file maintenance (#1588)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-04 05:07:49 +00:00
renovate[bot] 36a8646485 chore(deps): Update module github.com/libtnb/sqlite to v1.1.1 (#1586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-03 13:25:11 +00:00
renovate[bot] 901ce774d5 chore(deps): Update module github.com/fsnotify/fsnotify to v1.10.0 (#1585)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-03 05:49:35 +00:00
renovate[bot] 0a42e198dd chore(deps): Update non-major dependencies (#1584)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-02 16:35:28 +00:00
renovate[bot] 54ad4dff86 chore(deps): Update non-major dependencies (#1581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-30 21:57:37 +00:00
耗子 6f54b6e402 fix: De Morgan's law 2026-04-29 23:21:18 +08:00
耗子 075d9f8ee4 fix: nginx下ip证书签发失败 2026-04-29 20:27:09 +08:00
耗子 c0d8642cb5 fix: 优化代码 2026-04-28 06:05:34 +08:00
耗子 418d8af6d9 fix: 优化各列宽度 2026-04-28 04:18:39 +08:00
耗子 45b63f8cd0 fix: 容器镜像清理 2026-04-28 04:18:28 +08:00
耗子 521bf37a9f Merge remote-tracking branch 'origin/main' 2026-04-27 22:56:36 +08:00
耗子 452c5ffac8 fix: 更新错误页描述 2026-04-27 22:56:31 +08:00
h2zi f1cc5bb7d6 chore: update mocks 2026-04-27 12:31:25 +00:00
耗子 909556b3d6 refactor: 统一日志清理 2026-04-27 20:30:12 +08:00
耗子 cd64dc65f3 fix: lint 2026-04-27 19:32:20 +08:00
耗子 b3b29d79c5 fix: 优化supervisor管理器 2026-04-27 19:26:46 +08:00
耗子 d565d3bbb8 Merge remote-tracking branch 'origin/main' 2026-04-27 17:28:27 +08:00
耗子 dc62ecf1dd fix: shell 执行失败时丢失 cmd.Run 真实错误
当 cmd.Dir 不存在或 exec 启动失败时,Go 在进入 bash 前就返回错误,stderr 为空,原本只取 stderr 的错误格式会输出空 err,导致排查困难。改为同时携带 cmd.Run 的真实错误和 stderr。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 17:28:09 +08:00
renovate[bot] dd600d89d2 chore(deps): Lock file maintenance (#1580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 05:13:33 +00:00
renovate[bot] 1d4a488e83 chore(deps): Update non-major dependencies (#1579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-26 17:02:49 +00:00
renovate[bot] 71bb13cd66 chore(deps): Update non-major dependencies (#1578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-25 16:35:16 +00:00
renovate[bot] 5a52efd3ec chore(deps): Update dependency eslint-plugin-vue to v10.9.0 (#1577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-25 06:00:31 +00:00
renovate[bot] fc8de12590 chore(deps): Update module github.com/DeRuina/timberjack to v1.4.2 (#1576)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-24 14:04:04 +00:00
renovate[bot] b03ad4ba86 chore(deps): Update module github.com/go-webauthn/webauthn to v0.17.0 (#1575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-24 14:40:33 +08:00
renovate[bot] 2be313b676 chore(deps): Update dependency eslint-plugin-oxlint to v1.61.0 (#1574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 22:35:10 +00:00
renovate[bot] 5d8a08c507 chore(deps): Update non-major dependencies (#1573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-24 04:09:24 +08:00
renovate[bot] ab167e884c chore(deps): Update non-major dependencies (#1572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-24 04:06:33 +08:00
renovate[bot] b57602deb9 chore(deps): Update dependency vite to v8.0.9 (#1571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 08:50:16 +00:00
耗子 db8064fbe3 fix: 日志查看器改用 PTY + xterm 以支持滚动查看历史
journalctl 未加 -n 参数导致只能看到 10 行历史;watchEffect 每次日志更新
都强制滚到底部,导致用户即便有滚动条也无法回看历史。改用已有的 ws.pty +
xterm.js 基础设施取代 n-log,借助 xterm 的 scrollback buffer 获得与 SSH
下 tail 一致的交互体验。

- tail 预加载 1000 行并启用 -F 兼容日志轮转
- journalctl 加 --no-pager 防止 TTY 下启动分页器,预加载 1000 行
- 移除无用的 language prop 并同步清理 EditModal 的传参

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 01:45:24 +08:00
renovate[bot] aeabe9b17e chore(deps): Update dependency vue-tsc to v3.2.7 (#1570)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-22 23:18:48 +08:00
renovate[bot] cd65b13c0a chore(deps): Update module github.com/go-webauthn/webauthn to v0.16.5 (#1569)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-22 18:20:09 +08:00
563 changed files with 35757 additions and 29111 deletions
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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
+20 -215
View File
@@ -1,170 +1,18 @@
## 角色定义
你是 Linus TorvaldsLinux 内核的创造者和首席架构师。你已经维护 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
View File
@@ -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
}
+46 -39
View File
@@ -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
)
+109 -84
View File
@@ -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
View File
@@ -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
+5
View File
@@ -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 {
+6 -1
View File
@@ -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
}
+6
View File
@@ -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)
+6
View File
@@ -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 {
+6 -1
View File
@@ -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
}
+16 -11
View File
@@ -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,
+7
View File
@@ -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 {
+6
View File
@@ -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)
+35 -34
View File
@@ -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 添加条目
+5
View File
@@ -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 {
+4
View File
@@ -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()
}
+5
View File
@@ -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 {
+6
View File
@@ -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)
+5
View File
@@ -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
View File
@@ -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")
+6 -1
View File
@@ -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
}
+2 -4
View File
@@ -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 {
+7
View File
@@ -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)
}
+6 -1
View File
@@ -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
}
+4
View File
@@ -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()
}
+5
View File
@@ -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 {
+6
View File
@@ -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 {
+64 -29
View File
@@ -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")
+11 -7
View File
@@ -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
})
}
+14 -9
View File
@@ -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)
+11 -7
View File
@@ -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"]},
+6
View File
@@ -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")
+6
View File
@@ -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
View File
@@ -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
}
+62 -72
View File
@@ -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
}
+11 -7
View File
@@ -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"]},
+8 -6
View File
@@ -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 {
+1 -1
View File
@@ -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
}
+1
View File
@@ -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
+1
View File
@@ -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"
-1
View File
@@ -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
+5 -11
View File
@@ -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
+4 -9
View File
@@ -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
View File
@@ -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
}
+8 -8
View File
@@ -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
}
+2 -2
View File
@@ -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)
+16 -8
View File
@@ -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 清理未使用的容器
+8 -12
View File
@@ -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 -6
View File
@@ -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
}
+10
View File
@@ -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
}
+2 -2
View File
@@ -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)
+4
View File
@@ -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
+5 -8
View File
@@ -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 {
+4 -5
View File
@@ -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
}
+8
View File
@@ -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
View File
@@ -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)
}
+29 -8
View File
@@ -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
+34
View File
@@ -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 -7
View File
@@ -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 -3
View File
@@ -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"`
}
+14
View File
@@ -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"`
+1
View File
@@ -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
View File
@@ -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
}
+11 -13
View File
@@ -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
View File
@@ -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
}
+11 -9
View File
@@ -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
}
+5 -3
View File
@@ -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
}
// 更新分类缓存
+5 -3
View File
@@ -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
}
+10 -7
View File
@@ -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))
+3 -4
View File
@@ -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) {
+1
View File
@@ -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
View File
@@ -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)
}
+21 -4
View File
@@ -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
}
+1
View File
@@ -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"},
-16
View File
@@ -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)
+1 -1
View File
@@ -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
+4 -47
View File
@@ -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
View File
@@ -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,
})
}
+9 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+8 -6
View File
@@ -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)
}
+16
View File
@@ -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)
}
+4 -4
View File
@@ -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,
)
+4 -9
View File
@@ -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],
-15
View File
@@ -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 {
+7 -12
View File
@@ -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) })
+144
View File
@@ -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 -Fservice 给定时用 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
View File
@@ -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
}
+47
View File
@@ -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)
-46
View File
@@ -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
View File
@@ -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
View File
@@ -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 // dnsNamekeyAuth 列表
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
View File
@@ -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
View File
@@ -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
View File
@@ -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