Compare commits

..

126 Commits

Author SHA1 Message Date
耗子 5c77144f07 fix: 修复验证器规则报错 2026-07-03 08:59:04 +08:00
github-actions[bot] 94fe6753da New Crowdin translations by GitHub Action (#1697)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-07-03 04:48:47 +08:00
h2zi fe67fccde3 chore(l10n): update pot files 2026-07-02 20:29:58 +00:00
耗子 6b733fc2af fix(web): 修正 HealthBanner 中扫描感知的术语措辞
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 04:27:34 +08:00
h2zi 44f243e94f chore(l10n): update pot files 2026-07-02 20:22:59 +00:00
耗子 756834a59a chore(web): 同步 pnpm-lock 至 vite 8.1.3
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 04:21:14 +08:00
耗子 f024381660 feat(panel): 数据库故障降级、健康告警与检测
- 网站统计聚合器 flush 失败也 commit(丢弃已 drain 增量),防止 DB
  长期不可写时内存无界累积导致 OOM/CPU 100%
- 势态感知同样在 upsert 失败时上报健康问题
- 新增 internal/app.Health 全局健康状态注册表,通过 /home/health 暴露
- 前端 layout 顶部新增 HealthBanner,DB 故障时红色横幅提醒并给出修复引导
- acepanel fix 检测数据库损坏改用 PRAGMA quick_check,能识别 malformed
  等仅在查询时才暴露的静默损坏
- 主库与辅助库 DSN 一律设为 synchronous(FULL),减少宿主 write-back
  缓存丢失 fsync 导致的损坏概率
- 移除 debug.SetMemoryLimit(256MB):软上限在活对象超限时会驱动 GC 反复
  追赶不可回收对象,反而放大 CPU 打满

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 04:21:10 +08:00
耗子 3098d9fb64 fix(web): 升级 vite 至 8.1.3 修复生产构建白屏
vite 8.0.14+ 因 rolldown 1.0.2/1.0.3 tree-shaking 决策错误
(rolldown#9515),懒加载 chunk 调用未定义的 init_runtime_dom_esm_bundler
导致白屏。rolldown 1.1.2 (vite 8.1.0+) 修复。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 03:50:53 +08:00
github-actions[bot] fa71b6f4ef New Crowdin translations by GitHub Action (#1696)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-07-03 02:30:50 +08:00
h2zi 39b5f10012 chore(l10n): update pot files 2026-07-02 18:18:00 +00:00
h2zi 79fa002bd6 chore: update mocks 2026-07-02 18:15:21 +00:00
耗子 00a5acbc45 feat(panel): 重写自升级流程并精简面板备份 (close #1674)
升级改为覆盖式:只替换程序文件,storage 原地不动,中断安全无需 fix;新增 WS 实时进度,前端等待重启自动刷新。面板备份改为仅打包核心文件(ace/panel.db/config.yml/cert/cli),排除统计/扫描大库,体积大幅下降。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:12:52 +08:00
耗子 65e2750102 fix(website): 修复 IP 作为域名的处理(尤其 IPv6)
- 抽取 IsIPv6/WrapIPv6/UnwrapIPv6 到 pkg/tools,复用于 acme panelSolver
- server_name 写入时为 IPv6 套方括号、读取时剥离,Domains 统一裸地址口径,证书 SAN 判定自然正确
- 前端打开网站链接时为 IPv6 套方括号,避免拼出非法 URL

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 01:21:04 +08:00
耗子 93e013ec56 feat(file): 文件压缩/解压改为后台任务 (close #1675)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-03 00:51:27 +08:00
renovate[bot] 07e608a3c0 chore(deps): Update dependency tsx to v4.22.5 (#1695)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-03 00:32:26 +08:00
renovate[bot] a255e9e1d8 chore(deps): Update actions/checkout action to v7 (#1678)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-03 00:32:19 +08:00
耗子 f5ad988576 feat: 替换验证器实现 2026-07-03 00:22:23 +08:00
耗子 2e18c25054 feat(website): 纯静态网站默认写入 SPA 路由回退配置
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:58:31 +08:00
耗子 a98142ea93 fix(website): reload 失败时返回 web 服务配置测试详情
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:22:17 +08:00
耗子 4d8f6fad9f fix: X-Debug 调试日志仅在调试模式下生效
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:22:17 +08:00
renovate[bot] 67a7a41469 chore(deps): Update module github.com/bddjr/hlfhr to v1.6.1 (#1694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-02 19:54:47 +08:00
renovate[bot] afae33c69a chore(deps): Update module github.com/shirou/gopsutil/v4 to v4.26.6 (#1693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-07-02 00:56:45 +00:00
renovate[bot] 62c5271c37 chore(deps): Update dependency vue-tsc to v3.3.6 (#1692)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-30 23:05:34 +00:00
renovate[bot] ac116a7680 chore(deps): Update non-major dependencies (#1691)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-30 09:46:34 +00:00
renovate[bot] c1092576b2 chore(deps): Lock file maintenance (#1690)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-29 18:52:14 +08:00
renovate[bot] fea51108d2 chore(deps): Update module github.com/urfave/cli/v3 to v3.10.1 (#1689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-28 21:49:54 +08:00
renovate[bot] 69dae9be95 chore(deps): Update dependency vite-plugin-vue-devtools to v8.1.5 (#1688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-28 15:55:22 +08:00
耗子 e7b2323c70 Merge remote-tracking branch 'origin/main' 2026-06-28 15:53:15 +08:00
耗子 371f80451b fix: 移除部分默认配置 2026-06-28 15:53:08 +08:00
耗子 6e3b35b3db fix(acme): 修复 IP/通配符证书签发失败
- nginx server_name 中的 IPv6 字面量补上方括号,避免匹配不上 LE 带方括号的 Host 头被 default_server 接管
- Apache 改用 IP-based VirtualHost(ServerName 不支持 IPv6 字面量,会让 httpd -t 失败)
- DNS-01 改用 AppendRecords 单条追加(与 Caddy/certmagic 对齐),避免 SetRecords
  在 Cloudflare 等 provider 下按 (name,type) 覆盖同名 TXT,导致主域+通配符同时签发时
  主域 challenge 被通配符 challenge 覆盖

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-28 15:48:57 +08:00
renovate[bot] 2798d7fb5d chore(deps): Update dependency eslint to v10.6.0 (#1685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-27 01:57:16 +00:00
renovate[bot] f3696332af chore(deps): Update non-major dependencies (#1684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-26 17:02:38 +00:00
renovate[bot] a510b81cf5 chore(deps): Update non-major dependencies (#1683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-25 17:51:27 +00:00
renovate[bot] 4b675c587f chore(deps): Update non-major dependencies (#1682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-23 21:50:22 +00:00
renovate[bot] 3efc9e2f66 chore(deps): Update non-major dependencies (#1681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-23 01:52:45 +00:00
耗子 3715e6db5e fix(web): 修复网站编辑大小/时间单位反推导致的下拉失效与跳变
将单位作为独立 UI 状态保存到对象上(_size_unit / _units),
不再从字节/纳秒反推单位,避免:
- 空值切换 GB 等单位时因 buildSize(0,...)=0 导致单位丢失
- 输入 1024 MB 因恰好是 GB 整数倍而被自动跳成 1 GB
- 输入 60 秒被自动跳成 1 分钟等同类问题

覆盖 EditModal.vue 中 client_max_body_size 与 5 个 duration 字段,
以及 NginxIndex.vue 中 stream upstream resolver_timeout。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-23 02:41:01 +08:00
耗子 29247e6944 refactor(pkg/dns): 精简 netplan 结构体与去除接口遍历重复
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-22 17:37:44 +08:00
renovate[bot] 6303ac943a chore(deps): Lock file maintenance (#1680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-22 05:05:43 +00:00
renovate[bot] 8a89a40bd3 chore(deps): Update non-major dependencies (#1679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-22 01:59:38 +08:00
renovate[bot] 3589bec397 chore(deps): Update non-major dependencies (#1677)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-20 03:11:43 +00:00
耗子 c2a8200817 fix: 修复 CI lint 与前端类型检查报错
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-20 02:14:22 +08:00
renovate[bot] 4a22299111 chore(deps): Update non-major dependencies (#1676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-20 02:02:50 +08:00
耗子 54354c369a fix: fail2ban SSH 多 Port 时 jail 配置换行
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 20:17:22 +08:00
耗子 0181dadc05 fix: 文件上传前 precheck 冲突 支持跳过/重命名/覆盖
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-18 00:57:15 +08:00
耗子 30b593f90a Merge remote-tracking branch 'origin/main' 2026-06-17 18:24:29 +08:00
耗子 c22d6a430d fix: Chmod/Chown 递归操作前解锁 immutable 文件 2026-06-17 18:24:15 +08:00
renovate[bot] 08b09d58e8 chore(deps): Update non-major dependencies to v4.0.1 (#1671)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-16 16:00:07 +08:00
dependabot[bot] 3d3188630e chore(deps-dev): bump vite from 8.0.13 to 8.0.16 in /web (#1670)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.13 to 8.0.16.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 15:59:32 +08:00
renovate[bot] f5c23b8a2a chore(deps): Update non-major dependencies (#1668)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-16 03:50:02 +00:00
Copilot 78718db0a5 feat: info 命令仅首次运行重置密码,后续显示星号,支持 -f 强制重置 (#1669)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-16 01:32:53 +08:00
renovate[bot] 072cbb0132 chore(deps): Update non-major dependencies to v11.7.0 (#1667)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-15 22:41:20 +08:00
renovate[bot] c3732477be chore(deps): Update non-major dependencies (#1666)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-15 15:13:30 +08:00
renovate[bot] 03d617fea1 chore(deps): Lock file maintenance (#1665)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-15 05:50:15 +00:00
renovate[bot] 39f0d2564c chore(deps): Update non-major dependencies to v3.10.0 (#1664)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-14 16:59:38 +00:00
renovate[bot] c66cdfbc1f chore(deps): Update non-major dependencies to v8.1.3 (#1663)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-14 17:30:39 +08:00
renovate[bot] 97332f9650 chore(deps): Update non-major dependencies to v3.3.5 (#1662)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-13 13:56:45 +00:00
renovate[bot] be7aea4a68 chore(deps): Update non-major dependencies (#1661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-12 22:42:14 +00:00
renovate[bot] a71b4009fb chore(deps): Update major dependencies to v7 (#1658)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-12 16:42:32 +08:00
耗子 ef58fcd6eb fix: 证书签发使用 CA 默认证书链
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:50:33 +08:00
renovate[bot] e8d19a8171 chore(deps): Update non-major dependencies (#1660)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-12 09:08:09 +08:00
renovate[bot] d2ba8190ca chore(deps): Update non-major dependencies (#1659)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-10 22:00:32 +00:00
renovate[bot] 31bd29ae7a chore(deps): Update non-major dependencies to v4.0.0-rc.5 (#1657)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-09 17:41:23 +08:00
renovate[bot] 003feef87c chore(deps): Update non-major dependencies (#1656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-08 22:29:43 +00:00
耗子 0328215564 fix: 编辑网站问题优化 2026-06-08 23:06:06 +08:00
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
212 changed files with 13121 additions and 10492 deletions
+4 -3
View File
@@ -11,11 +11,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- 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
@@ -44,7 +45,7 @@ jobs:
fail-fast: true
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Go
+3 -2
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Go
@@ -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
+3 -2
View File
@@ -12,11 +12,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- 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
+5 -4
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
@@ -42,11 +42,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
run_install: true
run_install: |
- cwd: web
package_json_file: web/package.json
- name: Setup Node.js
uses: actions/setup-node@v6
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
+2 -2
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Setup Go
uses: actions/setup-go@v6
with:
@@ -28,7 +28,7 @@ jobs:
- name: Run tests
run: sudo go test -v -coverprofile="coverage.out" ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
disable_search: true
files: coverage.out
-1
View File
@@ -12,7 +12,6 @@ func main() {
}
debug.SetGCPercent(10)
debug.SetMemoryLimit(256 << 20)
ace, err := initAce()
if err != nil {
+6 -6
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,7 +106,7 @@ 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)
@@ -173,7 +173,7 @@ func initAce() (*app.Ace, error) {
containerImageService := service.NewContainerImageService(containerImageRepo)
containerVolumeRepo := data.NewContainerVolumeRepo(settingRepo)
containerVolumeService := service.NewContainerVolumeService(containerVolumeRepo)
fileService := service.NewFileService(locale, taskRepo)
fileService := service.NewFileService(locale, taskRepo, containerRepo)
logRepo := data.NewLogRepo(db)
logService := service.NewLogService(locale, logRepo)
monitorRepo := data.NewMonitorRepo(db, settingRepo)
@@ -198,7 +198,7 @@ func initAce() (*app.Ace, error) {
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)
wsService := service.NewWsService(locale, config, logger, sshRepo, settingRepo, certRepo, backupRepo, taskRepo)
ws := route.NewWs(wsService, toolboxMigrationService)
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
if err != nil {
@@ -218,7 +218,7 @@ func initAce() (*app.Ace, error) {
if err != nil {
return nil, err
}
validation := bootstrap.NewValidator(config, db)
ace := app.NewAce(config, mux, server, reloader, gormigrate, cron, taskRunner, validation)
validator := service.NewValidator(config, db)
ace := app.NewAce(config, mux, server, reloader, gormigrate, cron, taskRunner, validator)
return ace, nil
}
+36 -34
View File
@@ -3,12 +3,12 @@ module github.com/acepanel/panel/v3
go 1.26
require (
github.com/DeRuina/timberjack v1.4.2
github.com/andybalholm/brotli v1.2.1
github.com/bddjr/hlfhr v1.6.0
github.com/DeRuina/timberjack v1.4.5
github.com/andybalholm/brotli v1.2.2
github.com/bddjr/hlfhr v1.6.1
github.com/beevik/ntp v1.5.0
github.com/cilium/ebpf v0.21.0
github.com/coder/websocket v1.8.14
github.com/cilium/ebpf v0.22.0
github.com/coder/websocket v1.8.15
github.com/containerd/errdefs v1.0.0
github.com/coreos/go-systemd/v22 v22.7.0
github.com/creack/pty v1.1.24
@@ -16,14 +16,13 @@ require (
github.com/expr-lang/expr v1.17.8
github.com/fsnotify/fsnotify v1.10.1
github.com/go-chi/chi/v5 v5.3.0
github.com/go-chi/httplog/v3 v3.3.0
github.com/go-gormigrate/gormigrate/v2 v2.1.5
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.1
github.com/gookit/validate v1.5.7
github.com/hashicorp/go-version v1.9.0
github.com/leonelquinteros/gotext v1.7.2
github.com/lib/pq v1.12.3
@@ -39,34 +38,35 @@ 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/cron v0.2.1
github.com/libtnb/cron v0.2.2
github.com/libtnb/gormstore v1.2.1
github.com/libtnb/sessions v1.4.2
github.com/libtnb/sqlite v1.1.1
github.com/libtnb/sqlite v1.1.2
github.com/libtnb/utils v1.2.2
github.com/libtnb/validator v0.1.1
github.com/medama-io/go-useragent v1.2.3
github.com/mholt/acmez/v3 v3.1.6
github.com/moby/moby/api v1.54.2
github.com/moby/moby/client v0.4.1
github.com/moby/moby/api v1.55.0
github.com/moby/moby/client v0.5.0
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/samber/lo v1.53.0
github.com/sethvargo/go-limiter v1.1.0
github.com/shirou/gopsutil/v4 v4.26.4
github.com/shirou/gopsutil/v4 v4.26.6
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.9.0
github.com/urfave/cli/v3 v3.10.1
github.com/valyala/fastjson v1.6.10
go.yaml.in/yaml/v4 v4.0.0-rc.4
golang.org/x/crypto v0.52.0
golang.org/x/net v0.55.0
gorm.io/gorm v1.31.1
modernc.org/sqlite v1.50.1
resty.dev/v3 v3.0.0-beta.6
go.yaml.in/yaml/v4 v4.0.0-rc.6
golang.org/x/crypto v0.53.0
golang.org/x/net v0.56.0
golang.org/x/sync v0.21.0
gorm.io/gorm v1.31.2
modernc.org/sqlite v1.53.0
resty.dev/v3 v3.0.0-rc.2
)
require (
@@ -78,22 +78,23 @@ require (
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/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.6 // indirect
github.com/gofiber/schema v1.7.1 // indirect
github.com/gofiber/utils/v2 v2.0.0 // 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/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
@@ -110,7 +111,6 @@ require (
github.com/philhofer/fwd v1.2.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/timtadh/data-structures v0.6.2 // indirect
github.com/timtadh/lexmachine v0.2.3 // indirect
@@ -120,19 +120,21 @@ require (
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-20260508232706-74f9aab9d74a // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
modernc.org/libc v1.72.3 // 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.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
modernc.org/libc v1.73.4 // 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
)
+91 -72
View File
@@ -17,8 +17,8 @@ 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.2 h1:4bKlzhKdsR+2oNkgef9mqb4n11ICow8VK88RfzJPzN8=
github.com/DeRuina/timberjack v1.4.2/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=
@@ -26,13 +26,13 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bddjr/hlfhr v1.6.0 h1:0ZuSYc2X/YAVyvEVIfrORonWjC0ZY3x6gZAfe+ugBPM=
github.com/bddjr/hlfhr v1.6.0/go.mod h1:cHRve//dit7d8ZCPUUFZAYW5WCm3LdMoiqX9i9GJdms=
github.com/bddjr/hlfhr v1.6.1 h1:bzl0zsL2pl7Gvlbx6l8jZjjv9GbBTeOaX4/+RaXyNXc=
github.com/bddjr/hlfhr v1.6.1/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=
@@ -47,11 +47,11 @@ github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl
github.com/boyter/go-string v1.0.5 h1:/xcOlWdgelLYLVkUU0xBLfioGjZ9KIMUMI/RXG138YY=
github.com/boyter/go-string v1.0.5/go.mod h1:Mww9cDld2S2cdJ0tQffBhsZFMQRA2OJdcjWYZXvZ4Ss=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k=
github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ=
github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY=
github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -69,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=
@@ -86,6 +84,8 @@ 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=
@@ -100,16 +100,21 @@ github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9t
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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.3.0 h1:Gr6Y7nSzbpyCyRwKPOVKjDH3BH6TH5uvRNDsTZWDpvU=
github.com/go-chi/httplog/v3 v3.3.0/go.mod h1:N/J1l5l1fozUrqIVuT8Z/HzNeSy8TF2EFyokPLe6y2w=
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=
@@ -124,10 +129,10 @@ github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHl
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.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=
github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
github.com/gofiber/utils/v2 v2.0.0 h1:SCC3rpsEDWupFSHtc0RKxg/BKgV0s1qKfZg9Jv6D0sM=
github.com/gofiber/utils/v2 v2.0.0/go.mod h1:xF9v89FfmbrYqI/bQUGN7gR8ZtXot2jxnZvmAUtiavE=
github.com/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=
@@ -170,12 +175,6 @@ 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.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/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=
@@ -275,22 +274,22 @@ 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/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/cron v0.2.2 h1:f7HM3qaoEqFU1Dw1S2UFva1pmxgYOHOPS3Cw2tsbNi0=
github.com/libtnb/cron v0.2.2/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/moby/client v0.0.0-20260127104057-d4b7902a560b h1:73OlJdl4Zk6Ipnm7KKeE6yCjfd7nk0BhTArujx6WJCU=
github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b/go.mod h1:bBQRIiaGR9EZBC+x5hnwunNgcDOts7E1Oa7AowJwMFU=
github.com/libtnb/securecookie v1.2.0 h1:2uc0PBDm0foeSTrcZ9QTX1IEjf6kFEwfgEYSIXQSKrA=
github.com/libtnb/securecookie v1.2.0/go.mod h1:ja+wNGnQzYqcqXQnJWu6icsaWi5JEBwNEMJ2ReTVDxA=
github.com/libtnb/sessions v1.4.2 h1:KEYfuzEW7vtEedwvAZnDWWeW1yMSlwOUfp4hIUirFb8=
github.com/libtnb/sessions v1.4.2/go.mod h1:PvxiaI3PCD+kzjPmIrpV3s9e4ACps6OUaXgmKCYlUFo=
github.com/libtnb/sqlite v1.1.1 h1:p1Ud8vHTsPzgmChPUuJFkesrzl9ajD/pYrmoWf1cgig=
github.com/libtnb/sqlite v1.1.1/go.mod h1:PNlcqMVWU+pMIUR499fHvRLUdxrNVVVXwEZkW1c9stE=
github.com/libtnb/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.2 h1:HUqgAIb+TIrZPXRytYB5XqzkEXG2U4a4/ZGpprNtL5A=
github.com/libtnb/utils v1.2.2/go.mod h1:KUCx2+Phw3cLXOwuQDy6XwFOliaYuU1rpAuJQuHf+lQ=
github.com/libtnb/validator v0.1.1 h1:8D3YkPGR9N2Ildb2PeApvgzCgIfKznEb6cTHWPskmNs=
github.com/libtnb/validator v0.1.1/go.mod h1:j08Lydxwy7ModkoKeYIHxvZvAtnzzdE8xi2J+SGVrgc=
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=
@@ -298,6 +297,8 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
@@ -318,8 +319,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.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
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=
@@ -369,10 +372,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/shamaton/msgpack/v3 v3.0.0 h1:xl40uxWkSpwBCSTvS5wyXvJRsC6AcVcYeox9PspKiZg=
github.com/shamaton/msgpack/v3 v3.0.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/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.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
github.com/shirou/gopsutil/v4 v4.26.6/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=
@@ -412,8 +415,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.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c=
github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY=
github.com/urfave/cli/v3 v3.10.1/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=
@@ -428,28 +431,42 @@ 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=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
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-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw=
golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
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=
@@ -479,8 +496,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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
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=
@@ -489,8 +506,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -507,16 +524,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.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/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
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=
@@ -571,28 +588,30 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
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.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/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.3/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.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
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=
@@ -601,14 +620,14 @@ 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.50.1 h1:l+cQvn0sd0zJJtfygGHuQJ5AjlrwXmWPw4KP3ZMwr9w=
modernc.org/sqlite v1.50.1/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
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.2 h1:NE6RqPkUd27eQ5dFSb5tfyT3s6XX9ZySp9qZ7Nw1bn0=
resty.dev/v3 v3.0.0-rc.2/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+2 -2
View File
@@ -13,8 +13,8 @@ import (
"github.com/bddjr/hlfhr"
"github.com/go-chi/chi/v5"
"github.com/go-gormigrate/gormigrate/v2"
"github.com/gookit/validate"
"github.com/libtnb/cron"
"github.com/libtnb/validator"
"github.com/acepanel/panel/v3/pkg/config"
"github.com/acepanel/panel/v3/pkg/tlscert"
@@ -31,7 +31,7 @@ type Ace struct {
runner types.TaskRunner
}
func NewAce(conf *config.Config, router *chi.Mux, server *hlfhr.Server, reloader *tlscert.Reloader, migrator *gormigrate.Gormigrate, cron *cron.Cron, runner types.TaskRunner, _ *validate.Validation) *Ace {
func NewAce(conf *config.Config, router *chi.Mux, server *hlfhr.Server, reloader *tlscert.Reloader, migrator *gormigrate.Gormigrate, cron *cron.Cron, runner types.TaskRunner, _ *validator.Validator) *Ace {
return &Ace{
conf: conf,
router: router,
+73
View File
@@ -0,0 +1,73 @@
package app
import (
"sort"
"sync"
"time"
)
// HealthLevelError 需要用户立即处理
const HealthLevelError = "error"
// HealthLevelWarning 提示性问题,通常已被系统自动降级处理
const HealthLevelWarning = "warning"
// HealthIssue 单条健康问题
// Key 为稳定标识符(如 database:stat),前端据此选择翻译文案
// Message 为原始错误详情,供诊断参考
type HealthIssue struct {
Key string `json:"key"`
Level string `json:"level"`
Message string `json:"message"`
Since time.Time `json:"since"`
}
type healthRegistry struct {
mu sync.RWMutex
issues map[string]HealthIssue
}
// Health 全局健康状态注册表,供各后台任务上报/清除故障
var Health = &healthRegistry{issues: make(map[string]HealthIssue)}
// Report 上报或更新一条健康问题
// 同 key 重复上报时保留首次上报时间,仅更新 message 和 level
func (h *healthRegistry) Report(key, level, message string) {
h.mu.Lock()
defer h.mu.Unlock()
if existing, ok := h.issues[key]; ok {
existing.Level = level
existing.Message = message
h.issues[key] = existing
return
}
h.issues[key] = HealthIssue{
Key: key,
Level: level,
Message: message,
Since: time.Now(),
}
}
// Clear 清除指定 key 的健康问题
func (h *healthRegistry) Clear(key string) {
h.mu.Lock()
defer h.mu.Unlock()
delete(h.issues, key)
}
// Snapshot 返回当前所有健康问题,按 Since 升序(越早发生越靠前)
func (h *healthRegistry) Snapshot() []HealthIssue {
h.mu.RLock()
defer h.mu.RUnlock()
result := make([]HealthIssue, 0, len(h.issues))
for _, issue := range h.issues {
result = append(result, issue)
}
sort.Slice(result, func(i, j int) bool {
return result[i].Since.Before(result[j].Since)
})
return result
}
+1 -1
View File
@@ -66,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 -6
View File
@@ -7,21 +7,21 @@ type UpdateConfig struct {
// SetDefaultPassword 设置 default 用户密码
type SetDefaultPassword struct {
Password string `form:"password" json:"password" validate:"required|password"`
Password string `form:"password" json:"password" validate:"required && password"`
}
// ConfigTune ClickHouse 配置调整
type ConfigTune struct {
// 网络
ListenHost string `form:"listen_host" json:"listen_host"`
HTTPPort string `form:"http_port" json:"http_port"`
TCPPort string `form:"tcp_port" json:"tcp_port"`
HTTPPort string `form:"http_port" json:"http_port" validate:"number && min:1 && max:65535"`
TCPPort string `form:"tcp_port" json:"tcp_port" validate:"number && min:1 && max:65535"`
// 性能
MaxMemoryUsage string `form:"max_memory_usage" json:"max_memory_usage"`
MaxThreads string `form:"max_threads" json:"max_threads"`
// 路径
Path string `form:"path" json:"path"`
TmpPath string `form:"tmp_path" json:"tmp_path"`
Path string `form:"path" json:"path" validate:"unix_path"`
TmpPath string `form:"tmp_path" json:"tmp_path" validate:"unix_path"`
// 日志
LogLevel string `form:"log_level" json:"log_level"`
LogLevel string `form:"log_level" json:"log_level" validate:"in:trace,debug,information,warning,error"`
}
+16 -16
View File
@@ -12,22 +12,22 @@ type LogOpts struct {
// Settings Docker daemon 设置
type Settings struct {
RegistryMirrors []string `json:"registry-mirrors,omitempty"` // 注册表镜像
InsecureRegistries []string `json:"insecure-registries,omitempty"` // 非安全镜像仓库
LiveRestore bool `json:"live-restore,omitempty"` // Live restore
LogDriver string `json:"log-driver,omitempty"` // 日志驱动
LogOpts LogOpts `json:"log-opts"` // 日志配置选项
CgroupDriver string `json:"cgroup-driver,omitempty"` // cgroup 驱动(从 exec-opts 中提取)
Hosts []string `json:"hosts,omitempty"` // Socket 路径
DataRoot string `json:"data-root,omitempty"` // 数据目录
StorageDriver string `json:"storage-driver,omitempty"` // 存储驱动
DNS []string `json:"dns,omitempty"` // DNS 配置
FirewallBackend string `json:"firewall-backend,omitempty"` // 防火墙后端 (iptables/nftables)
Iptables *bool `json:"iptables,omitempty"` // iptables 规则
Ip6tables *bool `json:"ip6tables,omitempty"` // ip6tables 规则
IpForward *bool `json:"ip-forward,omitempty"` // IP 转发
IPv6 *bool `json:"ipv6,omitempty"` // IPv6 支持
Bip string `json:"bip,omitempty"` // 默认 bridge 网络 IP 段
RegistryMirrors []string `json:"registry-mirrors,omitempty"` // 注册表镜像
InsecureRegistries []string `json:"insecure-registries,omitempty"` // 非安全镜像仓库
LiveRestore bool `json:"live-restore,omitempty"` // Live restore
LogDriver string `json:"log-driver,omitempty"` // 日志驱动
LogOpts LogOpts `json:"log-opts"` // 日志配置选项
CgroupDriver string `json:"cgroup-driver,omitempty" validate:"in:systemd,cgroupfs"` // cgroup 驱动(从 exec-opts 中提取)
Hosts []string `json:"hosts,omitempty"` // Socket 路径
DataRoot string `json:"data-root,omitempty"` // 数据目录
StorageDriver string `json:"storage-driver,omitempty"` // 存储驱动
DNS []string `json:"dns,omitempty"` // DNS 配置
FirewallBackend string `json:"firewall-backend,omitempty" validate:"in:iptables,nftables"` // 防火墙后端 (iptables/nftables)
Iptables *bool `json:"iptables,omitempty"` // iptables 规则
Ip6tables *bool `json:"ip6tables,omitempty"` // ip6tables 规则
IpForward *bool `json:"ip-forward,omitempty"` // IP 转发
IPv6 *bool `json:"ipv6,omitempty"` // IPv6 支持
Bip string `json:"bip,omitempty"` // 默认 bridge 网络 IP 段
}
// UpdateSettings 更新设置请求
+1 -1
View File
@@ -57,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
}
+1 -1
View File
@@ -177,7 +177,7 @@ ignoreregex =
switch jailName {
case "ssh":
filter = "sshd"
port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}' | paste -sd ','")
case "mysql":
filter = "mysqld-auth"
port, err = shell.Execf("cat %s/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'", app.Root)
+7 -7
View File
@@ -2,12 +2,12 @@ package fail2ban
type Add struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
MaxRetry int `json:"maxretry" validate:"required"`
FindTime int `json:"findtime" validate:"required"`
BanTime int `json:"bantime" validate:"required"`
WebsiteName string `json:"website_name"`
WebsiteMode string `json:"website_mode"`
Type string `json:"type" validate:"required && in:service,website"`
MaxRetry int `json:"maxretry" validate:"required && min:1"`
FindTime int `json:"findtime" validate:"required && min:1"`
BanTime int `json:"bantime" validate:"required && min:1"`
WebsiteName string `json:"website_name" validate:"required_if:Type,website"`
WebsiteMode string `json:"website_mode" validate:"required_if:Type,website && in:cc,path"`
WebsitePath string `json:"website_path"`
}
@@ -21,7 +21,7 @@ type BanList struct {
type Unban struct {
Name string `json:"name" validate:"required"`
IP string `json:"ip" validate:"required"`
IP string `json:"ip" validate:"required && ip"`
}
type SetWhiteList struct {
+5 -5
View File
@@ -1,16 +1,16 @@
package frp
type Name struct {
Name string `form:"name" json:"name" validate:"required|in:frps,frpc"`
Name string `form:"name" json:"name" validate:"required && in:frps,frpc"`
}
type UpdateConfig struct {
Name string `form:"name" json:"name" validate:"required|in:frps,frpc"`
Name string `form:"name" json:"name" validate:"required && in:frps,frpc"`
Config string `form:"config" json:"config" validate:"required"`
}
type UpdateUser struct {
Name string `form:"name" json:"name" validate:"required|in:frps,frpc"`
User string `form:"user" json:"user" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Group string `form:"group" json:"group" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Name string `form:"name" json:"name" validate:"required && in:frps,frpc"`
User string `form:"user" json:"user" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
Group string `form:"group" json:"group" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
}
+1 -1
View File
@@ -67,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
}
+2 -2
View File
@@ -37,9 +37,9 @@ type ConfigTune struct {
// DataSource 数据源
type DataSource struct {
Name string `form:"name" json:"name" validate:"required"`
Type string `form:"type" json:"type" validate:"required|in:prometheus,mysql,postgres,influxdb,loki,elasticsearch,tempo,jaeger,zipkin,graphite,alertmanager,opentsdb,mssql,testdata,grafana-pyroscope-datasource,grafana-opensearch-datasource"`
Type string `form:"type" json:"type" validate:"required && in:prometheus,mysql,postgres,influxdb,loki,elasticsearch,tempo,jaeger,zipkin,graphite,alertmanager,opentsdb,mssql,testdata,grafana-pyroscope-datasource,grafana-opensearch-datasource"`
URL string `form:"url" json:"url" validate:"required"`
Access string `form:"access" json:"access"`
Access string `form:"access" json:"access" validate:"in:proxy,direct"`
IsDefault bool `form:"is_default" json:"is_default"`
// 数据库类型专用
Database string `form:"database" json:"database"`
+5 -5
View File
@@ -6,10 +6,10 @@ type UpdateConfig struct {
// ConfigTune Memcached 配置调整
type ConfigTune struct {
Port string `form:"port" json:"port"`
UDPPort string `form:"udp_port" json:"udp_port"`
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
UDPPort string `form:"udp_port" json:"udp_port" validate:"number && min:1 && max:65535"`
ListenAddress string `form:"listen_address" json:"listen_address"`
Memory string `form:"memory" json:"memory"`
MaxConnections string `form:"max_connections" json:"max_connections"`
Threads string `form:"threads" json:"threads"`
Memory string `form:"memory" json:"memory" validate:"number && min:1"`
MaxConnections string `form:"max_connections" json:"max_connections" validate:"number && min:1"`
Threads string `form:"threads" json:"threads" validate:"number && min:1"`
}
+5 -5
View File
@@ -7,19 +7,19 @@ type UpdateConfig struct {
// SetAdminPassword 设置 admin 密码
type SetAdminPassword struct {
Password string `form:"password" json:"password" validate:"required|password"`
Password string `form:"password" json:"password" validate:"required && password"`
}
// ConfigTune MongoDB 配置调整
type ConfigTune struct {
// 存储
DbPath string `form:"db_path" json:"db_path"`
DbPath string `form:"db_path" json:"db_path" validate:"unix_path"`
CacheSizeGB string `form:"cache_size_gb" json:"cache_size_gb"`
// 网络
Port string `form:"port" json:"port"`
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
BindIp string `form:"bind_ip" json:"bind_ip"`
// 日志
SystemLogPath string `form:"system_log_path" json:"system_log_path"`
SystemLogPath string `form:"system_log_path" json:"system_log_path" validate:"unix_path"`
// 安全
Authorization string `form:"authorization" json:"authorization"`
Authorization string `form:"authorization" json:"authorization" validate:"in:enabled,disabled"`
}
+24 -24
View File
@@ -247,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")
@@ -369,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")
+3 -3
View File
@@ -5,13 +5,13 @@ type UpdateConfig struct {
}
type SetRootPassword struct {
Password string `form:"password" json:"password" validate:"required|password"`
Password string `form:"password" json:"password" validate:"required && password"`
}
// ConfigTune MySQL 配置调整
type ConfigTune struct {
// 常规设置
Port string `form:"port" json:"port"`
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
MaxConnections string `form:"max_connections" json:"max_connections"`
MaxConnectErrors string `form:"max_connect_errors" json:"max_connect_errors"`
DefaultStorageEngine string `form:"default_storage_engine" json:"default_storage_engine"`
@@ -32,7 +32,7 @@ type ConfigTune struct {
// InnoDB
InnodbBufferPoolSize string `form:"innodb_buffer_pool_size" json:"innodb_buffer_pool_size"`
InnodbLogBufferSize string `form:"innodb_log_buffer_size" json:"innodb_log_buffer_size"`
InnodbFlushLogAtTrxCommit string `form:"innodb_flush_log_at_trx_commit" json:"innodb_flush_log_at_trx_commit"`
InnodbFlushLogAtTrxCommit string `form:"innodb_flush_log_at_trx_commit" json:"innodb_flush_log_at_trx_commit" validate:"in:0,1,2"`
InnodbLockWaitTimeout string `form:"innodb_lock_wait_timeout" json:"innodb_lock_wait_timeout"`
InnodbMaxDirtyPagesPct string `form:"innodb_max_dirty_pages_pct" json:"innodb_max_dirty_pages_pct"`
InnodbReadIoThreads string `form:"innodb_read_io_threads" json:"innodb_read_io_threads"`
+1 -1
View File
@@ -103,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
}
+15 -15
View File
@@ -39,22 +39,22 @@ type ConfigTune struct {
}
type StreamServer struct {
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 配置名称,用于文件命名
Listen string `form:"listen" json:"listen" validate:"required"` // 监听地址,如: "12345", "0.0.0.0:12345", "[::]:12345"
UDP bool `form:"udp" json:"udp"` // 是否 UDP 协议
ProxyPass string `form:"proxy_pass" json:"proxy_pass" validate:"required"` // 代理地址,如: "127.0.0.1:3306", "upstream_name"
ProxyProtocol bool `form:"proxy_protocol" json:"proxy_protocol"` // 是否启用 PROXY 协议
ProxyTimeout time.Duration `form:"proxy_timeout" json:"proxy_timeout"` // 代理超时时间
ProxyConnectTimeout time.Duration `form:"proxy_connect_timeout" json:"proxy_connect_timeout"` // 代理连接超时时间
SSL bool `form:"ssl" json:"ssl"` // 是否启用 SSL
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate" validate:"requiredIf:SSL,true"` // SSL 证书路径
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key" validate:"requiredIf:SSL,true"` // SSL 私钥路径
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""` // 配置名称,用于文件命名
Listen string `form:"listen" json:"listen" validate:"required"` // 监听地址,如: "12345", "0.0.0.0:12345", "[::]:12345"
UDP bool `form:"udp" json:"udp"` // 是否 UDP 协议
ProxyPass string `form:"proxy_pass" json:"proxy_pass" validate:"required"` // 代理地址,如: "127.0.0.1:3306", "upstream_name"
ProxyProtocol bool `form:"proxy_protocol" json:"proxy_protocol"` // 是否启用 PROXY 协议
ProxyTimeout time.Duration `form:"proxy_timeout" json:"proxy_timeout"` // 代理超时时间
ProxyConnectTimeout time.Duration `form:"proxy_connect_timeout" json:"proxy_connect_timeout"` // 代理连接超时时间
SSL bool `form:"ssl" json:"ssl"` // 是否启用 SSL
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate" validate:"required_if:SSL,true"` // SSL 证书路径
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key" validate:"required_if:SSL,true"` // SSL 私钥路径
}
type StreamUpstream struct {
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 上游名称
Servers map[string]string `form:"servers" json:"servers" validate:"required"` // 上游服务器及配置,如: map["127.0.0.1:3306"] = "weight=5"
Algo string `form:"algo" json:"algo"` // 负载均衡算法,如: "least_conn", "hash $remote_addr"
Resolver []string `form:"resolver" json:"resolver"` // DNS 解析器,如: ["8.8.8.8", "ipv6=off"]
ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""` // 上游名称
Servers map[string]string `form:"servers" json:"servers" validate:"required"` // 上游服务器及配置,如: map["127.0.0.1:3306"] = "weight=5"
Algo string `form:"algo" json:"algo"` // 负载均衡算法,如: "least_conn", "hash $remote_addr"
Resolver []string `form:"resolver" json:"resolver"` // DNS 解析器,如: ["8.8.8.8", "ipv6=off"]
ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间
}
+1 -1
View File
@@ -57,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
}
+1 -1
View File
@@ -5,5 +5,5 @@ type UpdateConfig struct {
}
type UpdatePort struct {
Port uint `form:"port" json:"port" validate:"required|number|min:1|max:65535"`
Port uint `form:"port" json:"port" validate:"required && number && min:1 && max:65535"`
}
+54 -19
View File
@@ -9,6 +9,7 @@ import (
"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"
@@ -55,7 +56,7 @@ func (s *App) Status() string {
// 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
@@ -72,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
}
@@ -136,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
@@ -152,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},
@@ -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")
+2 -2
View File
@@ -5,14 +5,14 @@ type UpdateConfig struct {
}
type SetPostgresPassword struct {
Password string `form:"password" json:"password" validate:"required|password"`
Password string `form:"password" json:"password" validate:"required && password"`
}
// ConfigTune PostgreSQL 配置调整
type ConfigTune struct {
// 连接设置
ListenAddresses string `form:"listen_addresses" json:"listen_addresses"`
Port string `form:"port" json:"port"`
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
MaxConnections string `form:"max_connections" json:"max_connections"`
SuperuserReservedConnections string `form:"superuser_reserved_connections" json:"superuser_reserved_connections"`
// 内存设置
+1 -1
View File
@@ -73,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
}
+9 -9
View File
@@ -2,7 +2,7 @@ package pureftpd
type Create struct {
Username string `form:"username" json:"username" validate:"required"`
Password string `form:"password" json:"password" validate:"required|password"`
Password string `form:"password" json:"password" validate:"required && password"`
Path string `form:"path" json:"path" validate:"required"`
}
@@ -12,21 +12,21 @@ type Delete struct {
type ChangePassword struct {
Username string `form:"username" json:"username" validate:"required"`
Password string `form:"password" json:"password" validate:"required|password"`
Password string `form:"password" json:"password" validate:"required && password"`
}
type UpdatePort struct {
Port uint `form:"port" json:"port" validate:"required|number|min:1|max:65535"`
Port uint `form:"port" json:"port" validate:"required && number && min:1 && max:65535"`
}
// ConfigTune Pure-FTPd 配置调整
type ConfigTune struct {
MaxClientsNumber string `form:"max_clients_number" json:"max_clients_number"`
MaxClientsPerIP string `form:"max_clients_per_ip" json:"max_clients_per_ip"`
MaxIdleTime string `form:"max_idle_time" json:"max_idle_time"`
MaxClientsNumber string `form:"max_clients_number" json:"max_clients_number" validate:"number && min:1"`
MaxClientsPerIP string `form:"max_clients_per_ip" json:"max_clients_per_ip" validate:"number && min:1"`
MaxIdleTime string `form:"max_idle_time" json:"max_idle_time" validate:"number && min:0"`
MaxLoad string `form:"max_load" json:"max_load"`
PassivePortRange string `form:"passive_port_range" json:"passive_port_range"`
AnonymousOnly string `form:"anonymous_only" json:"anonymous_only"`
NoAnonymous string `form:"no_anonymous" json:"no_anonymous"`
MaxDiskUsage string `form:"max_disk_usage" json:"max_disk_usage"`
AnonymousOnly string `form:"anonymous_only" json:"anonymous_only" validate:"in:yes,no"`
NoAnonymous string `form:"no_anonymous" json:"no_anonymous" validate:"in:yes,no"`
MaxDiskUsage string `form:"max_disk_usage" json:"max_disk_usage" validate:"number && min:1 && max:100"`
}
+7 -7
View File
@@ -8,15 +8,15 @@ type UpdateConfig struct {
type ConfigTune struct {
// 常规设置
Bind string `form:"bind" json:"bind"`
Port string `form:"port" json:"port"`
Databases string `form:"databases" json:"databases"`
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
Databases string `form:"databases" json:"databases" validate:"number && min:1"`
Requirepass string `form:"requirepass" json:"requirepass"`
Timeout string `form:"timeout" json:"timeout"`
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive"`
Timeout string `form:"timeout" json:"timeout" validate:"number"`
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive" validate:"number"`
// 内存
Maxmemory string `form:"maxmemory" json:"maxmemory"`
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy"`
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy" validate:"in:noeviction,allkeys-lru,allkeys-lfu,allkeys-random,volatile-lru,volatile-lfu,volatile-random,volatile-ttl"`
// 持久化
Appendonly string `form:"appendonly" json:"appendonly"`
Appendfsync string `form:"appendfsync" json:"appendfsync"`
Appendonly string `form:"appendonly" json:"appendonly" validate:"in:yes,no"`
Appendfsync string `form:"appendfsync" json:"appendfsync" validate:"in:always,everysec,no"`
}
+8 -8
View File
@@ -8,15 +8,15 @@ type UpdateConfig struct {
// ConfigTune RocketMQ 配置调整
type ConfigTune struct {
// Broker 基础
BrokerName string `form:"broker_name" json:"broker_name"` // brokerName
ListenPort string `form:"listen_port" json:"listen_port"` // listenPort
NamesrvAddr string `form:"namesrv_addr" json:"namesrv_addr"` // namesrvAddr
BrokerRole string `form:"broker_role" json:"broker_role"` // brokerRole
FlushDiskType string `form:"flush_disk_type" json:"flush_disk_type"` // flushDiskType
BrokerName string `form:"broker_name" json:"broker_name"` // brokerName
ListenPort string `form:"listen_port" json:"listen_port" validate:"number && min:1 && max:65535"` // listenPort
NamesrvAddr string `form:"namesrv_addr" json:"namesrv_addr"` // namesrvAddr
BrokerRole string `form:"broker_role" json:"broker_role" validate:"in:ASYNC_MASTER,SYNC_MASTER,SLAVE"` // brokerRole
FlushDiskType string `form:"flush_disk_type" json:"flush_disk_type" validate:"in:ASYNC_FLUSH,SYNC_FLUSH"` // flushDiskType
// 存储
StorePathRootDir string `form:"store_path_root_dir" json:"store_path_root_dir"` // storePathRootDir
StorePathCommitLog string `form:"store_path_commit_log" json:"store_path_commit_log"` // storePathCommitLog
MaxMessageSize string `form:"max_message_size" json:"max_message_size"` // maxMessageSize
StorePathRootDir string `form:"store_path_root_dir" json:"store_path_root_dir" validate:"unix_path"` // storePathRootDir
StorePathCommitLog string `form:"store_path_commit_log" json:"store_path_commit_log" validate:"unix_path"` // storePathCommitLog
MaxMessageSize string `form:"max_message_size" json:"max_message_size" validate:"number"` // maxMessageSize
// JVM - NameServer
NamesrvHeapInitSize string `form:"namesrv_heap_init_size" json:"namesrv_heap_init_size"` // -Xms (namesrv)
NamesrvHeapMaxSize string `form:"namesrv_heap_max_size" json:"namesrv_heap_max_size"` // -Xmx (namesrv)
+2 -2
View File
@@ -4,8 +4,8 @@ type Create struct {
Ak string `form:"ak" json:"ak" validate:"required"`
Sk string `form:"sk" json:"sk" validate:"required"`
Bucket string `form:"bucket" json:"bucket" validate:"required"`
URL string `form:"url" json:"url" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
URL string `form:"url" json:"url" validate:"required && url"`
Path string `form:"path" json:"path" validate:"required && unix_path"`
}
type Delete struct {
+2 -2
View File
@@ -16,7 +16,7 @@ type ProcessName struct {
type CreateProcess struct {
Name string `form:"name" json:"name" validate:"required"`
User string `form:"user" json:"user" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
Path string `form:"path" json:"path" validate:"required && unix_path"`
Command string `form:"command" json:"command" validate:"required"`
Num int `form:"num" json:"num" validate:"required|min:1"`
Num int `form:"num" json:"num" validate:"required && min:1"`
}
+7 -7
View File
@@ -8,15 +8,15 @@ type UpdateConfig struct {
type ConfigTune struct {
// 常规设置
Bind string `form:"bind" json:"bind"`
Port string `form:"port" json:"port"`
Databases string `form:"databases" json:"databases"`
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
Databases string `form:"databases" json:"databases" validate:"number && min:1"`
Requirepass string `form:"requirepass" json:"requirepass"`
Timeout string `form:"timeout" json:"timeout"`
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive"`
Timeout string `form:"timeout" json:"timeout" validate:"number"`
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive" validate:"number"`
// 内存
Maxmemory string `form:"maxmemory" json:"maxmemory"`
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy"`
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy" validate:"in:noeviction,allkeys-lru,allkeys-lfu,volatile-lru,volatile-lfu,allkeys-random,volatile-random,volatile-ttl"`
// 持久化
Appendonly string `form:"appendonly" json:"appendonly"`
Appendfsync string `form:"appendfsync" json:"appendfsync"`
Appendonly string `form:"appendonly" json:"appendonly" validate:"in:yes,no"`
Appendfsync string `form:"appendfsync" json:"appendfsync" validate:"in:always,everysec,no"`
}
+9 -7
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 {
@@ -29,5 +31,5 @@ type BackupRepo interface {
CutoffUpload(account uint, typ BackupType, name string, files []string) error
GetDefaultPath(typ BackupType) string
FixPanel() error
UpdatePanel(version, url, checksum string) error
UpdatePanel(version, url, checksum string, progress func(string)) error
}
+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
@@ -50,6 +50,7 @@ const (
SettingKeyIPDBType SettingKey = "ipdb_type" // "" / "custom" / "subscribe"
SettingKeyIPDBURL SettingKey = "ipdb_url" // 订阅链接
SettingKeyIPDBPath SettingKey = "ipdb_path"
SettingKeyInfoRan SettingKey = "info_ran" // info 命令是否已运行过
)
type Setting struct {
+1 -1
View File
@@ -7,4 +7,4 @@ import (
)
// ProviderSet is bootstrap providers.
var ProviderSet = wire.NewSet(NewConf, NewT, NewLog, NewCli, NewValidator, NewRouter, NewTLSReloader, NewHttp, NewDB, NewMigrate, NewLoader, NewSession, NewCron, NewRunner, websitestat.NewAggregator)
var ProviderSet = wire.NewSet(NewConf, NewT, NewLog, NewCli, NewRouter, NewTLSReloader, NewHttp, NewDB, NewMigrate, NewLoader, NewSession, NewCron, NewRunner, websitestat.NewAggregator)
+1 -1
View File
@@ -32,7 +32,7 @@ func NewDB(conf *config.Config) (*gorm.DB, error) {
options = append(options, sloggorm.WithTraceAll())
}
db, err := gorm.Open(sqlite.Open("file:"+filepath.Join(app.Root, "panel/storage/panel.db")+"?_txlock=immediate&_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)"),
db, err := gorm.Open(sqlite.Open("file:"+filepath.Join(app.Root, "panel/storage/panel.db")+"?_txlock=immediate&_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=synchronous(FULL)"),
&gorm.Config{
Logger: sloggorm.New(options...),
SkipDefaultTransaction: true,
-34
View File
@@ -1,34 +0,0 @@
package bootstrap
import (
"github.com/gookit/validate"
"github.com/gookit/validate/locales/ruru"
"github.com/gookit/validate/locales/zhcn"
"github.com/gookit/validate/locales/zhtw"
"gorm.io/gorm"
"github.com/acepanel/panel/v3/internal/http/rule"
"github.com/acepanel/panel/v3/pkg/config"
)
// NewValidator just for register global rules
func NewValidator(conf *config.Config, db *gorm.DB) *validate.Validation {
switch conf.App.Locale {
case "zh_CN":
zhcn.RegisterGlobal()
case "zh_TW":
zhtw.RegisterGlobal()
case "ru_RU":
ruru.RegisterGlobal()
}
validate.Config(func(opt *validate.GlobalOption) {
opt.StopOnError = false
opt.SkipOnEmpty = true
})
// register global rules
rule.GlobalRules(db)
return validate.NewEmpty()
}
+585 -162
View File
@@ -9,6 +9,7 @@ import (
"path/filepath"
"slices"
"strings"
"sync/atomic"
"time"
"github.com/leonelquinteros/gotext"
@@ -22,18 +23,20 @@ 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"
)
type backupRepo struct {
hr string
t *gotext.Locale
conf *config.Config
db *gorm.DB
log *slog.Logger
setting biz.SettingRepo
website biz.WebsiteRepo
hr string
t *gotext.Locale
conf *config.Config
db *gorm.DB
log *slog.Logger
setting biz.SettingRepo
website biz.WebsiteRepo
updating atomic.Bool // 面板升级进行中标志,防止并发触发
}
func NewBackupRepo(t *gotext.Locale, conf *config.Config, db *gorm.DB, log *slog.Logger, setting biz.SettingRepo, website biz.WebsiteRepo) biz.BackupRepo {
@@ -127,6 +130,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:
@@ -171,26 +180,34 @@ 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%s", time.Now().Format("20060102150405"), r.backupExt()))
backup := filepath.Join(r.GetDefaultPath(biz.BackupTypePanel), fmt.Sprintf("panel_%s.tar.xz", time.Now().Format("20060102150405")))
temp, err := os.MkdirTemp("", "ace-backup-*")
if err != nil {
return err
}
defer func(path string) { _ = os.RemoveAll(path) }(temp)
// 备份前 checkpoint 主库,尽量减少 -wal 中未落盘数据(热备份一致性)
_ = r.db.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error
if err = io.Cp(filepath.Join(app.Root, "panel"), temp); err != nil {
return err
}
if err = io.Cp("/usr/local/sbin/acepanel", temp); err != nil {
return err
// 只备份恢复面板运行所必需的核心文件;checkpoint 后 -wal 已并入 panel.db、
// -shm 为纯临时共享内存,均无需备份
var files []string
for _, f := range []string{
"panel/ace",
"panel/storage/panel.db",
"panel/storage/config.yml",
"panel/storage/cert.pem",
"panel/storage/cert.key",
} {
if io.Exists(filepath.Join(app.Root, f)) {
files = append(files, f)
}
}
_ = io.Chmod(temp, 0600)
if err = io.Compress(temp, nil, backup); err != nil {
// 两个 -C 把 panel 内的核心文件与 panel 外的 cli 二进制收进同一个包
if _, err := shell.Execf(
"tar -cJf '%s' -C '%s' %s -C /usr/local/sbin acepanel",
backup, app.Root, strings.Join(files, " "),
); err != nil {
return err
}
if err = io.Chmod(backup, 0600); err != nil {
if err := io.Chmod(backup, 0600); err != nil {
return err
}
@@ -237,6 +254,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"))
}
@@ -260,9 +283,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 切割日志
@@ -627,6 +652,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) {
@@ -766,6 +904,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-*")
@@ -802,10 +1210,9 @@ func (r *backupRepo) FixPanel() error {
// 检查关键文件是否正常
panelBroken := !io.Exists(filepath.Join(app.Root, "panel", "ace")) ||
!io.Exists(filepath.Join(app.Root, "panel", "storage", "config.yml")) ||
!io.Exists(filepath.Join(app.Root, "panel", "storage", "panel.db")) ||
io.Exists("/tmp/panel-storage.zip")
// 检查主数据库连接
if err := r.db.Exec("VACUUM").Error; err != nil {
!io.Exists(filepath.Join(app.Root, "panel", "storage", "panel.db"))
// 检查主数据库完整性
if !quickCheck(r.db) {
panelBroken = true
}
if err := r.db.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
@@ -816,13 +1223,17 @@ func (r *backupRepo) FixPanel() error {
var brokenAuxDBs []string
for _, name := range []string{"stat", "scan"} {
auxDB, err := openDB(name)
if err == nil {
if sqlDB, dbErr := auxDB.DB(); dbErr == nil {
_ = sqlDB.Close()
}
if err != nil {
brokenAuxDBs = append(brokenAuxDBs, name)
continue
}
brokenAuxDBs = append(brokenAuxDBs, name)
ok := quickCheck(auxDB)
if sqlDB, dbErr := auxDB.DB(); dbErr == nil {
_ = sqlDB.Close()
}
if !ok {
brokenAuxDBs = append(brokenAuxDBs, name)
}
}
// 一切正常,无需修复
@@ -853,22 +1264,6 @@ func (r *backupRepo) FixPanel() error {
return nil
}
// 再次确认是否需要修复
if io.Exists("/tmp/panel-storage.zip") {
// 文件齐全情况下只移除临时文件
if io.Exists(filepath.Join(app.Root, "panel", "ace")) &&
io.Exists(filepath.Join(app.Root, "panel", "storage", "config.yml")) &&
io.Exists(filepath.Join(app.Root, "panel", "storage", "panel.db")) {
if err := io.Remove("/tmp/panel-storage.zip"); err != nil {
return errors.New(r.t.Get("failed to clean temporary files: %v", err))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Cleaned up temporary files, please run acepanel update to update the panel"))
}
return nil
}
}
// 从备份目录中找最新的备份文件
files, err := os.ReadDir(r.GetDefaultPath(biz.BackupTypePanel))
if err != nil {
@@ -876,6 +1271,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
@@ -923,19 +1321,6 @@ func (r *backupRepo) FixPanel() error {
}
}
// tmp 目录下如果有 storage 备份,则解压回去
if app.IsCli {
fmt.Println(r.t.Get("|-Restore panel data..."))
}
if io.Exists("/tmp/panel-storage.zip") {
if err = io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel")); err != nil {
return errors.New(r.t.Get("Unzip panel data failed: %v", err))
}
if err = io.Remove("/tmp/panel-storage.zip"); err != nil {
return errors.New(r.t.Get("Cleaning temporary file failed: %v", err))
}
}
// 下载服务文件
if !io.Exists("/etc/systemd/system/acepanel.service") {
if _, err = shell.Execf(`wget -O /etc/systemd/system/acepanel.service https://%s/acepanel.service && sed -i "s|/opt/ace|%s|g" /etc/systemd/system/acepanel.service`, r.conf.App.DownloadEndpoint, app.Root); err != nil {
@@ -975,132 +1360,170 @@ func (r *backupRepo) FixPanel() error {
return nil
}
func (r *backupRepo) UpdatePanel(version, url, checksum string) error {
// 预先优化数据库
if err := r.db.Exec("VACUUM").Error; err != nil {
// UpdatePanel 升级面板
func (r *backupRepo) UpdatePanel(version, url, checksum string, progress func(string)) error {
if progress == nil {
progress = func(string) {}
}
// 进程级升级锁
if !r.updating.CompareAndSwap(false, true) {
return errors.New(r.t.Get("panel is already updating, please try again later"))
}
defer r.updating.Store(false)
panelDir := filepath.Join(app.Root, "panel")
workDir := filepath.Join(panelDir, ".update-work") // staging 目录固定在 panel 内,绝不用 /tmp(可能跨分区)
newDir := filepath.Join(workDir, "new")
name := filepath.Base(url)
// 失败回滚
rollback := func(err error) error {
_ = io.Remove(workDir)
app.Status = app.StatusNormal
return err
}
app.Status = app.StatusUpgrade
progress(r.t.Get("Preparing to update to %s...", version))
if err := io.Remove(workDir); err != nil {
return rollback(errors.New(r.t.Get("Failed to clean up temporary directory: %v", err)))
}
if err := r.db.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
return err
return rollback(errors.New(r.t.Get("Failed to optimize database: %v", err)))
}
name := filepath.Base(url)
if app.IsCli {
fmt.Println(r.t.Get("|-Target version: %s", version))
fmt.Println(r.t.Get("|-Download link: %s", url))
fmt.Println(r.t.Get("|-File name: %s", name))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Downloading..."))
}
if _, err := shell.Execf("aria2c -c --file-allocation=falloc --allow-overwrite=true --auto-file-renaming=false --retry-wait=5 --max-tries=5 -x 16 -s 16 -k 1M -d /tmp -o %s %s", name, url); err != nil {
return errors.New(r.t.Get("Download failed: %v", err))
}
if _, err := shell.Execf("aria2c -c --file-allocation=falloc --allow-overwrite=true --auto-file-renaming=false --retry-wait=5 --max-tries=5 -x 1 -s 1 -k 1M -d /tmp -o %s %s", name+".sha256", checksum); err != nil {
return errors.New(r.t.Get("Download failed: %v", err))
}
if !io.Exists(filepath.Join("/tmp", name)) || !io.Exists(filepath.Join("/tmp", name+".sha256")) {
return errors.New(r.t.Get("Download file check failed"))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Verify download file..."))
}
if check, err := shell.Execf("cd /tmp && sha256sum -c %s --ignore-missing", name+".sha256"); check != name+": OK" || err != nil {
return errors.New(r.t.Get("Verify download file failed: %v", err))
}
if err := io.Remove(filepath.Join("/tmp", name+".sha256")); err != nil {
return errors.New(r.t.Get("|-Clean up verification file failed: %v", err))
}
if io.Exists("/tmp/panel-storage.zip") {
return errors.New(r.t.Get("Temporary file detected in /tmp, this may be caused by the last update failure, please run acepanel fix to repair and try again"))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Backup panel data..."))
}
// 备份面板
// 备份留档
progress(r.t.Get("Backing up panel data..."))
if err := r.CreatePanel(); err != nil {
return errors.New(r.t.Get("|-Backup panel data failed: %v", err))
}
if err := io.Compress(filepath.Join(app.Root, "panel/storage"), nil, "/tmp/panel-storage.zip"); err != nil {
return errors.New(r.t.Get("|-Backup panel data failed: %v", err))
}
if !io.Exists("/tmp/panel-storage.zip") {
return errors.New(r.t.Get("|-Backup panel data failed, missing file"))
r.log.Warn("failed to backup panel before update", slog.Any("err", err))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Cleaning old version..."))
// 下载新版本
if err := os.MkdirAll(workDir, 0700); err != nil {
return rollback(errors.New(r.t.Get("Failed to create temporary directory: %v", err)))
}
if _, err := shell.Execf("rm -rf %s/panel/*", app.Root); err != nil {
return errors.New(r.t.Get("|-Cleaning old version failed: %v", err))
progress(r.t.Get("Downloading new version..."))
if _, err := shell.Execf("aria2c -c --file-allocation=falloc --allow-overwrite=true --auto-file-renaming=false --retry-wait=5 --max-tries=5 -x 16 -s 16 -k 1M -d %s -o %s %s", workDir, name, url); err != nil {
return rollback(errors.New(r.t.Get("Download failed: %v", err)))
}
if _, err := shell.Execf("aria2c -c --file-allocation=falloc --allow-overwrite=true --auto-file-renaming=false --retry-wait=5 --max-tries=5 -x 1 -s 1 -k 1M -d %s -o %s %s", workDir, name+".sha256", checksum); err != nil {
return rollback(errors.New(r.t.Get("Download failed: %v", err)))
}
if !io.Exists(filepath.Join(workDir, name)) || !io.Exists(filepath.Join(workDir, name+".sha256")) {
return rollback(errors.New(r.t.Get("Download file check failed")))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Unzip new version..."))
}
if err := io.UnCompress(filepath.Join("/tmp", name), filepath.Join(app.Root, "panel")); err != nil {
return errors.New(r.t.Get("|-Unzip new version failed: %v", err))
}
if !io.Exists(filepath.Join(app.Root, "panel", "ace")) {
return errors.New(r.t.Get("|-Unzip new version failed, missing file"))
}
if err := io.Remove(filepath.Join("/tmp", name)); err != nil {
return errors.New(r.t.Get("|-Clean up temporary file failed: %v", err))
// 校验 sha256
progress(r.t.Get("Verifying download file..."))
if check, err := shell.ExecfWithDir(workDir, "sha256sum -c %s --ignore-missing", name+".sha256"); check != name+": OK" || err != nil {
return rollback(errors.New(r.t.Get("Verify download file failed: %v", err)))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Restore panel data..."))
// 解压
progress(r.t.Get("Extracting new version..."))
if err := io.UnCompress(filepath.Join(workDir, name), newDir); err != nil {
return rollback(errors.New(r.t.Get("Unzip new version failed: %v", err)))
}
if err := io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel", "storage")); err != nil {
return errors.New(r.t.Get("|-Restore panel data failed: %v", err))
}
if !io.Exists(filepath.Join(app.Root, "panel/storage/panel.db")) {
return errors.New(r.t.Get("|-Restore panel data failed, missing file"))
if !io.Exists(filepath.Join(newDir, "ace")) {
return rollback(errors.New(r.t.Get("Unzip new version failed, missing file")))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Run post-update script..."))
}
if _, err := shell.Execf("curl -sSLm 10 https://%s/auto_update.sh | bash", r.conf.App.DownloadEndpoint); err != nil {
return errors.New(r.t.Get("|-Run post-update script failed: %v", err))
}
if _, err := shell.Execf(
`wget -O /etc/systemd/system/acepanel.service https://%s/acepanel.service && sed -i "s|/www|%s|g" /etc/systemd/system/acepanel.service`,
r.conf.App.DownloadEndpoint, app.Root,
); err != nil {
return errors.New(r.t.Get("|-Download panel service file failed: %v", err))
}
if _, err := shell.Execf("acepanel setting write version %s", version); err != nil {
return errors.New(r.t.Get("|-Write new panel version failed: %v", err))
}
if err := io.Mv(filepath.Join(app.Root, "panel/cli"), "/usr/local/sbin/acepanel"); err != nil {
return errors.New(r.t.Get("|-Move acepanel tool failed: %v", err))
// 应用
progress(r.t.Get("Applying update..."))
if err := r.applyUpdate(newDir); err != nil {
return rollback(errors.New(r.t.Get("Applying update failed: %v", err)))
}
if app.IsCli {
fmt.Println(r.t.Get("|-Set key file permissions..."))
}
_ = io.Chmod("/usr/local/sbin/acepanel", 0700)
_ = io.Chmod("/etc/systemd/system/acepanel.service", 0644)
_ = io.Chmod(filepath.Join(app.Root, "panel"), 0700)
if app.IsCli {
fmt.Println(r.t.Get("|-Update completed"))
// 收尾
progress(r.t.Get("Finishing up..."))
if err := r.finishUpdate(version); err != nil {
return rollback(errors.New(r.t.Get("Finishing update failed: %v", err)))
}
_ = io.Remove(workDir)
r.log.Info("panel updated", slog.String("version", version))
progress(r.t.Get("Update completed"))
_, _ = shell.Execf("systemctl daemon-reload")
_ = io.Remove("/tmp/panel-storage.zip")
_ = io.Remove(filepath.Join(app.Root, "panel/config.example.yml"))
// 由调用方重启面板
if sqlDB, err := r.db.DB(); err == nil {
_ = sqlDB.Close()
}
tools.RestartPanel()
return nil
}
// applyUpdate 用 newDir 下的新版本文件替换 panel/ 中的程序文件
func (r *backupRepo) applyUpdate(newDir string) error {
entries, err := os.ReadDir(newDir)
if err != nil {
return err
}
panelDir := filepath.Join(app.Root, "panel")
for _, e := range entries {
name := e.Name()
if name == "storage" {
continue
}
src := filepath.Join(newDir, name)
if name == "cli" {
// 先 cp 保证原子替换
tmp := "/usr/local/sbin/.acepanel.new"
if err = io.Cp(src, tmp); err != nil {
return err
}
if err = io.Mv(tmp, "/usr/local/sbin/acepanel"); err != nil {
return err
}
continue
}
// 其余程序文件
if err = io.Mv(src, filepath.Join(panelDir, name)); err != nil {
return err
}
}
return nil
}
// finishUpdate 升级收尾
func (r *backupRepo) finishUpdate(version string) error {
panelDir := filepath.Join(app.Root, "panel")
serviceFile := "/etc/systemd/system/acepanel.service"
// 更新 service 文件
tmpService := serviceFile + ".new"
if _, err := shell.Execf(`wget -O %s https://%s/acepanel.service`, tmpService, r.conf.App.DownloadEndpoint); err == nil {
_, _ = shell.Execf(`sed -i "s|/opt/ace|%s|g" %s`, app.Root, tmpService)
if out, _ := shell.Execf("grep -c ExecStart %s", tmpService); strings.TrimSpace(out) != "0" {
_ = io.Mv(tmpService, serviceFile) // 同在 /etc/systemd/system → 同分区 rename
}
}
_ = io.Remove(tmpService)
if !io.Exists(serviceFile) {
return errors.New(r.t.Get("panel service file is missing"))
}
// 校验 unit 指向的主程序确实存在,避免 daemon-reload 后起不来
if !io.Exists(filepath.Join(panelDir, "ace")) {
return errors.New(r.t.Get("panel binary is missing after update"))
}
_, _ = shell.Execf("systemctl daemon-reload")
// 执行后置脚本
_, _ = shell.Execf("curl -sSLm 10 --fail --retry 3 https://%s/auto_update.sh | bash", r.conf.App.DownloadEndpoint)
// 后置脚本之后再写版本号
if err := r.setting.Set(biz.SettingKeyVersion, version); err != nil {
return err
}
// 设置权限
_ = io.Chmod(filepath.Join(panelDir, "ace"), 0700)
_ = io.Chmod("/usr/local/sbin/acepanel", 0700)
_ = io.Chmod(serviceFile, 0644)
_ = io.Remove(filepath.Join(panelDir, "config.example.yml"))
// 修正可能从 staging 继承的错误 SELinux 上下文
_, _ = shell.Execf("restorecon %s /usr/local/sbin/acepanel %s", filepath.Join(panelDir, "ace"), serviceFile)
return nil
}
+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 清理未使用的容器
+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
+10 -1
View File
@@ -55,7 +55,7 @@ func getOperatorID(ctx context.Context) uint64 {
// openDB 打开数据库
func openDB(name string) (*gorm.DB, error) {
dsn := "file:" + filepath.Join(app.Root, fmt.Sprintf("panel/storage/%s.db", name)) +
"?_txlock=immediate&_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)"
"?_txlock=immediate&_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=synchronous(FULL)"
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true,
DisableForeignKeyConstraintWhenMigrating: true,
@@ -69,6 +69,15 @@ func openDB(name string) (*gorm.DB, error) {
return db, nil
}
// quickCheck 用 PRAGMA quick_check 检测 SQLite 数据库是否损坏
func quickCheck(db *gorm.DB) bool {
var result string
if err := db.Raw("PRAGMA quick_check").Row().Scan(&result); err != nil {
return false
}
return result == "ok"
}
// upsert 分批大小,避免超出 SQLite 变量数限制
const upsertBatchSize = 100
+42 -16
View File
@@ -28,6 +28,7 @@ import (
"github.com/acepanel/panel/v3/pkg/punycode"
"github.com/acepanel/panel/v3/pkg/shell"
"github.com/acepanel/panel/v3/pkg/systemctl"
"github.com/acepanel/panel/v3/pkg/tools"
"github.com/acepanel/panel/v3/pkg/types"
"github.com/acepanel/panel/v3/pkg/webserver"
webservertypes "github.com/acepanel/panel/v3/pkg/webserver/types"
@@ -147,6 +148,7 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
setting.Listens = vhost.Listen()
// 域名
domains := vhost.ServerName()
domains = lo.Map(domains, func(d string, _ int) string { return tools.UnwrapIPv6(d) })
domains, err = punycode.DecodeDomains(domains)
if err != nil {
return nil, err
@@ -201,7 +203,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 +228,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
}
@@ -322,6 +324,7 @@ func (r *websiteRepo) Create(ctx context.Context, req *request.WebsiteCreate) (*
if err != nil {
return nil, err
}
domains = lo.Map(domains, func(d string, _ int) string { return tools.WrapIPv6(d) })
if err = vhost.SetServerName(domains); err != nil {
return nil, err
}
@@ -344,7 +347,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 +371,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,11 +423,33 @@ 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
}
}
// 纯静态网站默认写入单页应用(SPA)前端路由回退配置
if w.Type == biz.WebsiteTypeStatic {
var spaConfig string
switch webServer {
case "nginx":
spaConfig = `# single-page application route fallback, remove if not needed
location / {
try_files $uri $uri/ /index.html;
}
`
case "apache":
spaConfig = `# single-page application route fallback, remove if not needed
FallbackResource /index.html
`
}
if spaConfig != "" {
if err = vhost.SetRawConfig("800-spa.conf", webservertypes.ScopeSite, spaConfig); err != nil {
return nil, err
}
}
}
// 初始化网站目录
if err = os.MkdirAll(req.Path, 0755); err != nil {
return nil, err
@@ -472,7 +497,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
}
@@ -582,6 +607,7 @@ func (r *websiteRepo) Update(ctx context.Context, req *request.WebsiteUpdate) er
if err != nil {
return err
}
domains = lo.Map(domains, func(d string, _ int) string { return tools.WrapIPv6(d) })
if err = vhost.SetServerName(domains); err != nil {
return err
}
@@ -665,7 +691,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 +791,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)
}
}
@@ -870,7 +896,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 {
@@ -1202,13 +1228,13 @@ func (r *websiteRepo) reloadWebServer() error {
switch webServer {
case "nginx":
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return err
out, _ := shell.Execf("nginx -t")
return fmt.Errorf("failed to reload nginx: %w; config test: %s", err, out)
}
case "apache":
if err = systemctl.Reload("apache"); err != nil {
_, err = shell.Execf("apachectl configtest")
return err
out, _ := shell.Execf("apachectl configtest")
return fmt.Errorf("failed to reload apache: %w; config test: %s", err, out)
}
default:
return errors.New(r.t.Get("unsupported web server: %s", webServer))
@@ -1300,9 +1326,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)
}
+2 -2
View File
@@ -62,10 +62,10 @@ func (r *Middlewares) Globals(t *gotext.Locale, mux *chi.Mux) []func(http.Handle
return respStatus == 404 || respStatus == 405
},
LogRequestBody: func(req *http.Request) bool {
return req.Header.Get("X-Debug-Request") == "1"
return r.conf.App.Debug && req.Header.Get("X-Debug-Request") == "1"
},
LogResponseBody: func(req *http.Request) bool {
return req.Header.Get("X-Debug-Response") == "1"
return r.conf.App.Debug && req.Header.Get("X-Debug-Response") == "1"
},
}),
compressor.Handler,
+2 -2
View File
@@ -1,7 +1,7 @@
package request
type App struct {
Slug string `json:"slug" form:"slug" validate:"required|notExists:apps,slug"`
Slug string `json:"slug" form:"slug" validate:"required && not_exists:apps,slug"`
Channel string `json:"channel" form:"channel" validate:"required"`
}
@@ -14,7 +14,7 @@ type AppSlugs struct {
}
type AppUpdateShow struct {
Slug string `json:"slug" form:"slug" validate:"required|exists:apps,slug"`
Slug string `json:"slug" form:"slug" validate:"required && exists:apps,slug"`
Show bool `json:"show" form:"show"`
}
+5 -5
View File
@@ -3,12 +3,12 @@ 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"`
Target string `json:"target" form:"target" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
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,11 +18,11 @@ 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"`
}
type BackupRestore struct {
BackupFile
Target string `json:"target" form:"target" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Target string `json:"target" form:"target" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
}
+3 -3
View File
@@ -3,14 +3,14 @@ package request
import "github.com/acepanel/panel/v3/pkg/types"
type BackupStorageCreate struct {
Type string `form:"type" json:"type" validate:"required|in:s3,sftp,webdav"`
Type string `form:"type" json:"type" validate:"required && in:s3,sftp,webdav"`
Name string `form:"name" json:"name" validate:"required"`
Info types.BackupStorageInfo `form:"info" json:"info"`
}
type BackupStorageUpdate struct {
ID uint `form:"id" json:"id" validate:"required|exists:backup_storages,id"`
Type string `form:"type" json:"type" validate:"required|in:s3,sftp,webdav"`
ID uint `form:"id" json:"id" validate:"required && exists:backup_storages,id"`
Type string `form:"type" json:"type" validate:"required && in:s3,sftp,webdav"`
Name string `form:"name" json:"name" validate:"required"`
Info types.BackupStorageInfo `form:"info" json:"info"`
}
+7 -21
View File
@@ -1,15 +1,13 @@
package request
import "net/http"
type CertUpload struct {
Cert string `form:"cert" json:"cert" validate:"required"`
Key string `form:"key" json:"key" validate:"required"`
}
type CertCreate struct {
Type string `form:"type" json:"type" validate:"required|in:P256,P384,2048,3072,4096"`
Domains []string `form:"domains" json:"domains" validate:"required|isSlice"`
Type string `form:"type" json:"type" validate:"required && in:P256,P384,2048,3072,4096"`
Domains []string `form:"domains" json:"domains" validate:"required && dive && required"`
Alias map[string]string `form:"alias" json:"alias"`
AutoRenewal bool `form:"auto_renewal" json:"auto_renewal"`
AccountID uint `form:"account_id" json:"account_id"`
@@ -17,16 +15,10 @@ type CertCreate struct {
WebsiteID uint `form:"website_id" json:"website_id"`
}
func (r *CertCreate) Rules(_ *http.Request) map[string]string {
return map[string]string{
"Domains.*": "required",
}
}
type CertUpdate struct {
ID uint `form:"id" json:"id" validate:"required|exists:certs,id"`
Type string `form:"type" json:"type" validate:"required|in:P256,P384,2048,3072,4096,upload"`
Domains []string `form:"domains" json:"domains" validate:"required|isSlice"`
ID uint `form:"id" json:"id" validate:"required && exists:certs,id"`
Type string `form:"type" json:"type" validate:"required && in:P256,P384,2048,3072,4096,upload"`
Domains []string `form:"domains" json:"domains" validate:"required && dive && required"`
Alias map[string]string `form:"alias" json:"alias"`
Cert string `form:"cert" json:"cert"`
Key string `form:"key" json:"key"`
@@ -37,14 +29,8 @@ type CertUpdate struct {
WebsiteID uint `form:"website_id" json:"website_id"`
}
func (r *CertUpdate) Rules(_ *http.Request) map[string]string {
return map[string]string{
"Domains.*": "required",
}
}
type CertDeploy struct {
ID uint `form:"id" json:"id" validate:"required|exists:certs,id"`
WebsiteID uint `form:"website_id" json:"website_id" validate:"required|exists:websites,id"`
ID uint `form:"id" json:"id" validate:"required && exists:certs,id"`
WebsiteID uint `form:"website_id" json:"website_id" validate:"required && exists:websites,id"`
EnableHTTPS bool `form:"enable_https" json:"enable_https"`
}
+7 -7
View File
@@ -1,18 +1,18 @@
package request
type CertAccountCreate struct {
CA string `form:"ca" json:"ca" validate:"required|in:googlecn,google,letsencrypt,litessl,zerossl,sslcom"`
Email string `form:"email" json:"email" validate:"required"`
CA string `form:"ca" json:"ca" validate:"required && in:googlecn,google,letsencrypt,litessl,zerossl,sslcom"`
Email string `form:"email" json:"email" validate:"required && email"`
Kid string `form:"kid" json:"kid"`
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
KeyType string `form:"key_type" json:"key_type" validate:"required|in:P256,P384,2048,3072,4096"`
KeyType string `form:"key_type" json:"key_type" validate:"required && in:P256,P384,2048,3072,4096"`
}
type CertAccountUpdate struct {
ID uint `form:"id" json:"id" validate:"required|exists:cert_accounts,id"`
CA string `form:"ca" json:"ca" validate:"required|in:googlecn,google,letsencrypt,litessl,zerossl,sslcom"`
Email string `form:"email" json:"email" validate:"required"`
ID uint `form:"id" json:"id" validate:"required && exists:cert_accounts,id"`
CA string `form:"ca" json:"ca" validate:"required && in:googlecn,google,letsencrypt,litessl,zerossl,sslcom"`
Email string `form:"email" json:"email" validate:"required && email"`
Kid string `form:"kid" json:"kid"`
HmacEncoded string `form:"hmac_encoded" json:"hmac_encoded"`
KeyType string `form:"key_type" json:"key_type" validate:"required|in:P256,P384,2048,3072,4096"`
KeyType string `form:"key_type" json:"key_type" validate:"required && in:P256,P384,2048,3072,4096"`
}
+3 -3
View File
@@ -3,14 +3,14 @@ package request
import "github.com/acepanel/panel/v3/pkg/acme"
type CertDNSCreate struct {
Type acme.DnsType `form:"type" json:"type" validate:"required|in:aliyun,tencent,huawei,westcn,cloudflare,gcore,porkbun,namesilo,cloudns"`
Type acme.DnsType `form:"type" json:"type" validate:"required && in:aliyun,tencent,huawei,westcn,cloudflare,gcore,porkbun,namesilo,cloudns"`
Name string `form:"name" json:"name" validate:"required"`
Data acme.DNSParam `form:"data" json:"data" validate:"required"`
}
type CertDNSUpdate struct {
ID uint `form:"id" json:"id" validate:"required|exists:cert_dns,id"`
Type acme.DnsType `form:"type" json:"type" validate:"required|in:aliyun,tencent,huawei,westcn,cloudflare,gcore,porkbun,namesilo,cloudns"`
ID uint `form:"id" json:"id" validate:"required && exists:cert_dns,id"`
Type acme.DnsType `form:"type" json:"type" validate:"required && in:aliyun,tencent,huawei,westcn,cloudflare,gcore,porkbun,namesilo,cloudns"`
Name string `form:"name" json:"name" validate:"required"`
Data acme.DNSParam `form:"data" json:"data" validate:"required"`
}
+1 -1
View File
@@ -1,5 +1,5 @@
package request
type ID struct {
ID uint `json:"id" form:"id" query:"id" uri:"id" validate:"required|min:1"`
ID uint `json:"id" form:"id" query:"id" uri:"id" validate:"required && min:1"`
}
+2 -2
View File
@@ -8,7 +8,7 @@ type ContainerID struct {
type ContainerRename struct {
ID string `form:"id" json:"id" validate:"required"`
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
}
type ContainerCreate struct {
@@ -21,7 +21,7 @@ type ContainerCreate struct {
Env []types.KV `form:"env" json:"env"`
Entrypoint []string `form:"entrypoint" json:"entrypoint"`
Command []string `form:"command" json:"command"`
RestartPolicy string `form:"restart_policy" json:"restart_policy"`
RestartPolicy string `form:"restart_policy" json:"restart_policy" validate:"in:no,always,on-failure,unless-stopped"`
AutoRemove bool `form:"auto_remove" json:"auto_remove"`
Privileged bool `form:"privileged" json:"privileged"`
OpenStdin bool `form:"openStdin" json:"open_stdin"`
+6 -6
View File
@@ -3,30 +3,30 @@ package request
import "github.com/acepanel/panel/v3/pkg/types"
type ContainerComposeGet struct {
Name string `uri:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Name string `uri:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
}
type ContainerComposeCreate struct {
Name string `json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Name string `json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
Compose string `json:"compose" validate:"required"`
Envs []types.KV `json:"envs"`
}
type ContainerComposeUpdate struct {
Name string `uri:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Name string `uri:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
Compose string `json:"compose" validate:"required"`
Envs []types.KV `json:"envs"`
}
type ContainerComposeUp struct {
Name string `uri:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Name string `uri:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
Force bool `json:"force"`
}
type ContainerComposeDown struct {
Name string `uri:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Name string `uri:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
}
type ContainerComposeRemove struct {
Name string `uri:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Name string `uri:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
}
+2 -2
View File
@@ -7,6 +7,6 @@ type ContainerImageID struct {
type ContainerImagePull struct {
Name string `form:"name" json:"name" validate:"required"`
Auth bool `form:"auth" json:"auth"`
Username string `form:"username" json:"username" validate:"requiredIf:Auth,true"`
Password string `form:"password" json:"password" validate:"requiredIf:Auth,true"`
Username string `form:"username" json:"username" validate:"required_if:Auth,true"`
Password string `form:"password" json:"password" validate:"required_if:Auth,true"`
}
+2 -2
View File
@@ -7,8 +7,8 @@ type ContainerNetworkID struct {
}
type ContainerNetworkCreate struct {
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Driver string `form:"driver" json:"driver" validate:"required|in:bridge,host,overlay,macvlan,ipvlan,none"`
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
Driver string `form:"driver" json:"driver" validate:"required && in:bridge,host,overlay,macvlan,ipvlan,none"`
Ipv4 types.ContainerContainerNetwork `form:"ipv4" json:"ipv4"`
Ipv6 types.ContainerContainerNetwork `form:"ipv6" json:"ipv6"`
Labels []types.KV `form:"labels" json:"labels"`
+2 -2
View File
@@ -7,8 +7,8 @@ type ContainerVolumeID struct {
}
type ContainerVolumeCreate struct {
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Driver string `form:"driver" json:"driver" validate:"required|in:local"`
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
Driver string `form:"driver" json:"driver" validate:"required && in:local"`
Labels []types.KV `form:"labels" json:"labels"`
Options []types.KV `form:"options" json:"options"`
}
+9 -9
View File
@@ -1,14 +1,14 @@
package request
type CronCreate struct {
Name string `form:"name" json:"name" validate:"required|notExists:crons,name"`
Type string `form:"type" json:"type" validate:"required"`
Time string `form:"time" json:"time" validate:"required|cron"`
Name string `form:"name" json:"name" validate:"required && not_exists:crons,name"`
Type string `form:"type" json:"type" validate:"required && in:shell,backup,cutoff,url,synctime"`
Time string `form:"time" json:"time" validate:"required && cron"`
Script string `form:"script" json:"script"`
SubType string `form:"sub_type" json:"sub_type" validate:"requiredIf:Type,backup,cutoff"`
SubType string `form:"sub_type" json:"sub_type" validate:"required_if:Type,backup,cutoff"`
Flock bool `form:"flock" json:"flock"`
Storage uint `form:"storage" json:"storage"`
Targets []string `form:"targets" json:"targets" validate:"requiredIf:Type,backup,cutoff"`
Targets []string `form:"targets" json:"targets" validate:"required_if:Type,backup,cutoff"`
Keep uint `form:"keep" json:"keep" validate:"required"`
URL string `form:"url" json:"url"`
Method string `form:"method" json:"method"`
@@ -20,10 +20,10 @@ type CronCreate struct {
}
type CronUpdate struct {
ID uint `form:"id" json:"id" validate:"required|exists:crons,id"`
ID uint `form:"id" json:"id" validate:"required && exists:crons,id"`
Name string `form:"name" json:"name" validate:"required"`
Type string `form:"type" json:"type" validate:"required"`
Time string `form:"time" json:"time" validate:"required|cron"`
Type string `form:"type" json:"type" validate:"required && in:shell,backup,cutoff,url,synctime"`
Time string `form:"time" json:"time" validate:"required && cron"`
Script string `form:"script" json:"script"`
SubType string `form:"sub_type" json:"sub_type"`
Flock bool `form:"flock" json:"flock"`
@@ -40,6 +40,6 @@ type CronUpdate struct {
}
type CronStatus struct {
ID uint `form:"id" json:"id" validate:"required|exists:crons,id"`
ID uint `form:"id" json:"id" validate:"required && exists:crons,id"`
Status bool `form:"status" json:"status"`
}
+8 -8
View File
@@ -6,22 +6,22 @@ type DatabaseList struct {
}
type DatabaseCreate struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z_-][a-zA-Z0-9_-]*$\""`
CreateUser bool `form:"create_user" json:"create_user"`
Username string `form:"username" json:"username" validate:"requiredIf:CreateUser,true|not_in:root,admin|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
Password string `form:"password" json:"password" validate:"requiredIf:CreateUser,true"`
Username string `form:"username" json:"username" validate:"required_if:CreateUser,true && not_in:root,admin && regex:\"^[a-zA-Z_-][a-zA-Z0-9_-]*$\""`
Password string `form:"password" json:"password" validate:"required_if:CreateUser,true"`
Host string `form:"host" json:"host"`
Comment string `form:"comment" json:"comment"`
}
type DatabaseDelete struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z_-][a-zA-Z0-9_-]*$\""`
}
type DatabaseComment struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z_-][a-zA-Z0-9_-]*$\""`
Comment string `form:"comment" json:"comment"`
}
@@ -2,39 +2,39 @@ package request
// DatabaseESIndices 获取索引列表
type DatabaseESIndices struct {
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required && exists:database_servers,id"`
}
// DatabaseESIndexCreate 创建索引
type DatabaseESIndexCreate struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
Name string `form:"name" json:"name" validate:"required"`
}
// DatabaseESIndexDelete 删除索引
type DatabaseESIndexDelete struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
Name string `form:"name" json:"name" validate:"required"`
}
// DatabaseESData 搜索文档列表
type DatabaseESData struct {
Paginate
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required && exists:database_servers,id"`
Index string `form:"index" json:"index" query:"index" validate:"required"`
Search string `form:"search" json:"search" query:"search"`
}
// DatabaseESDocumentGet 获取文档
type DatabaseESDocumentGet struct {
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required && exists:database_servers,id"`
Index string `form:"index" json:"index" query:"index" validate:"required"`
ID string `form:"id" json:"id" query:"id" validate:"required"`
}
// DatabaseESDocumentSet 创建/更新文档
type DatabaseESDocumentSet struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
Index string `form:"index" json:"index" validate:"required"`
ID string `form:"id" json:"id"`
Body string `form:"body" json:"body" validate:"required"`
@@ -42,7 +42,7 @@ type DatabaseESDocumentSet struct {
// DatabaseESDocumentDelete 删除文档
type DatabaseESDocumentDelete struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
Index string `form:"index" json:"index" validate:"required"`
ID string `form:"id" json:"id" validate:"required"`
}
+9 -9
View File
@@ -1,52 +1,52 @@
package request
type DatabaseRedisDatabases struct {
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required && exists:database_servers,id"`
}
type DatabaseRedisData struct {
Paginate
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required && exists:database_servers,id"`
DB int `form:"db" json:"db" query:"db"`
Search string `form:"search" json:"search" query:"search"`
}
type DatabaseRedisKeyGet struct {
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" query:"server_id" validate:"required && exists:database_servers,id"`
DB int `form:"db" json:"db" query:"db"`
Key string `form:"key" json:"key" query:"key" validate:"required"`
}
type DatabaseRedisKeySet struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
DB int `form:"db" json:"db"`
Key string `form:"key" json:"key" validate:"required"`
Value string `form:"value" json:"value" validate:"required"`
Type string `form:"type" json:"type" validate:"required|in:string,list,set,zset,hash"`
Type string `form:"type" json:"type" validate:"required && in:string,list,set,zset,hash"`
TTL int64 `form:"ttl" json:"ttl"`
}
type DatabaseRedisKeyDelete struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
DB int `form:"db" json:"db"`
Key string `form:"key" json:"key" validate:"required"`
}
type DatabaseRedisKeyTTL struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
DB int `form:"db" json:"db"`
Key string `form:"key" json:"key" validate:"required"`
TTL int64 `form:"ttl" json:"ttl"`
}
type DatabaseRedisKeyRename struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
DB int `form:"db" json:"db"`
OldKey string `form:"old_key" json:"old_key" validate:"required"`
NewKey string `form:"new_key" json:"new_key" validate:"required"`
}
type DatabaseRedisClear struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
DB int `form:"db" json:"db"`
}
+7 -7
View File
@@ -1,26 +1,26 @@
package request
type DatabaseServerCreate struct {
Name string `form:"name" json:"name" validate:"required|notExists:database_servers,name|regex:^[a-zA-Z0-9_-]+$"`
Type string `form:"type" json:"type" validate:"required|in:mysql,postgresql,redis,clickhouse,mongodb,sqlite,elasticsearch"`
Name string `form:"name" json:"name" validate:"required && not_exists:database_servers,name && regex:\"^[a-zA-Z0-9_-]+$\""`
Type string `form:"type" json:"type" validate:"required && in:mysql,postgresql,redis,clickhouse,mongodb,sqlite,elasticsearch"`
Host string `form:"host" json:"host" validate:"required"`
Port uint `form:"port" json:"port" validate:"max:65535"`
Port uint `form:"port" json:"port" validate:"min:1 && max:65535"`
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
Remark string `form:"remark" json:"remark"`
}
type DatabaseServerUpdate struct {
ID uint `form:"id" json:"id" validate:"required|exists:database_servers,id"`
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
ID uint `form:"id" json:"id" validate:"required && exists:database_servers,id"`
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
Host string `form:"host" json:"host" validate:"required"`
Port uint `form:"port" json:"port" validate:"max:65535"`
Port uint `form:"port" json:"port" validate:"min:1 && max:65535"`
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
Remark string `form:"remark" json:"remark"`
}
type DatabaseServerUpdateRemark struct {
ID uint `form:"id" json:"id" validate:"required|exists:database_servers,id"`
ID uint `form:"id" json:"id" validate:"required && exists:database_servers,id"`
Remark string `form:"remark" json:"remark"`
}
+4 -4
View File
@@ -1,8 +1,8 @@
package request
type DatabaseUserCreate struct {
ServerID uint `form:"server_id" json:"server_id" validate:"required|exists:database_servers,id"`
Username string `form:"username" json:"username" validate:"required|not_in:root,admin|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
ServerID uint `form:"server_id" json:"server_id" validate:"required && exists:database_servers,id"`
Username string `form:"username" json:"username" validate:"required && not_in:root,admin && regex:\"^[a-zA-Z_-][a-zA-Z0-9_-]*$\""`
Password string `form:"password" json:"password" validate:"required"`
Host string `form:"host" json:"host"`
Privileges []string `form:"privileges" json:"privileges"`
@@ -10,13 +10,13 @@ type DatabaseUserCreate struct {
}
type DatabaseUserUpdate struct {
ID uint `form:"id" json:"id" validate:"required|exists:database_users,id"`
ID uint `form:"id" json:"id" validate:"required && exists:database_users,id"`
Password string `form:"password" json:"password"`
Privileges []string `form:"privileges" json:"privileges"`
Remark string `form:"remark" json:"remark"`
}
type DatabaseUserUpdateRemark struct {
ID uint `form:"id" json:"id" validate:"required|exists:database_users,id"`
ID uint `form:"id" json:"id" validate:"required && exists:database_users,id"`
Remark string `form:"remark" json:"remark"`
}
+11 -11
View File
@@ -27,21 +27,21 @@ type EnvironmentPHPConfigTune struct {
// php.ini 上传限制
UploadMaxFilesize string `form:"upload_max_filesize" json:"upload_max_filesize"`
PostMaxSize string `form:"post_max_size" json:"post_max_size"`
MaxFileUploads string `form:"max_file_uploads" json:"max_file_uploads"`
MaxFileUploads string `form:"max_file_uploads" json:"max_file_uploads" validate:"number"`
MemoryLimit string `form:"memory_limit" json:"memory_limit"`
// php.ini 超时限制
MaxExecutionTime string `form:"max_execution_time" json:"max_execution_time"`
MaxInputTime string `form:"max_input_time" json:"max_input_time"`
MaxInputVars string `form:"max_input_vars" json:"max_input_vars"`
MaxExecutionTime string `form:"max_execution_time" json:"max_execution_time" validate:"number"`
MaxInputTime string `form:"max_input_time" json:"max_input_time" validate:"number"`
MaxInputVars string `form:"max_input_vars" json:"max_input_vars" validate:"number"`
// php.ini Session 相关
SessionSaveHandler string `form:"session_save_handler" json:"session_save_handler"`
SessionSavePath string `form:"session_save_path" json:"session_save_path"`
SessionGcMaxlifetime string `form:"session_gc_maxlifetime" json:"session_gc_maxlifetime"`
SessionCookieLifetime string `form:"session_cookie_lifetime" json:"session_cookie_lifetime"`
SessionGcMaxlifetime string `form:"session_gc_maxlifetime" json:"session_gc_maxlifetime" validate:"number"`
SessionCookieLifetime string `form:"session_cookie_lifetime" json:"session_cookie_lifetime" validate:"number"`
// php-fpm.conf 相关
Pm string `form:"pm" json:"pm"`
PmMaxChildren string `form:"pm_max_children" json:"pm_max_children"`
PmStartServers string `form:"pm_start_servers" json:"pm_start_servers"`
PmMinSpareServers string `form:"pm_min_spare_servers" json:"pm_min_spare_servers"`
PmMaxSpareServers string `form:"pm_max_spare_servers" json:"pm_max_spare_servers"`
Pm string `form:"pm" json:"pm" validate:"in:static,dynamic,ondemand"`
PmMaxChildren string `form:"pm_max_children" json:"pm_max_children" validate:"number"`
PmStartServers string `form:"pm_start_servers" json:"pm_start_servers" validate:"number"`
PmMinSpareServers string `form:"pm_min_spare_servers" json:"pm_min_spare_servers" validate:"number"`
PmMaxSpareServers string `form:"pm_max_spare_servers" json:"pm_max_spare_servers" validate:"number"`
}
+32 -36
View File
@@ -7,7 +7,7 @@ import (
)
type FileList struct {
Path string `json:"path" form:"path" validate:"required|isUnixPath"`
Path string `json:"path" form:"path" validate:"required && unix_path"`
Sort string `json:"sort" form:"sort"`
Keyword string `form:"keyword" json:"keyword"`
Sub bool `form:"sub" json:"sub"`
@@ -19,80 +19,76 @@ func (r *FileList) Prepare(req *http.Request) error {
}
type FilePath struct {
Path string `json:"path" form:"path" validate:"required|isUnixPath"`
Path string `json:"path" form:"path" validate:"required && unix_path"`
}
type FileTail struct {
Path string `json:"path" form:"path"`
Service string `json:"service" form:"service"`
Offset int `json:"offset" form:"offset"`
Limit int `json:"limit" form:"limit"`
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"`
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"`
Path string `json:"path" form:"path" validate:"required && unix_path"`
}
type FileSave struct {
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
Path string `form:"path" json:"path" validate:"required && unix_path"`
Content string `form:"content" json:"content"`
}
type FileControl struct {
Source string `form:"source" json:"source" validate:"required|isUnixPath"`
Target string `form:"target" json:"target" validate:"required|isUnixPath"`
Source string `form:"source" json:"source" validate:"required && unix_path"`
Target string `form:"target" json:"target" validate:"required && unix_path"`
Force bool `form:"force" json:"force"`
}
type FileRemoteDownload struct {
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
URL string `form:"url" json:"url" validate:"required|isFullURL"`
Path string `form:"path" json:"path" validate:"required && unix_path"`
URL string `form:"url" json:"url" validate:"required && url"`
}
type FilePermission struct {
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
Path string `form:"path" json:"path" validate:"required && unix_path"`
Mode string `form:"mode" json:"mode" validate:"required"`
Owner string `form:"owner" json:"owner" validate:"required"`
Group string `form:"group" json:"group" validate:"required"`
}
type FileCompress struct {
Dir string `form:"dir" json:"dir" validate:"required|isUnixPath"`
Paths []string `form:"paths" json:"paths" validate:"required|isSlice"`
File string `form:"file" json:"file" validate:"required|isUnixPath"`
}
func (r *FileCompress) Rules(_ *http.Request) map[string]string {
return map[string]string{
"Paths.*": "required",
}
Dir string `form:"dir" json:"dir" validate:"required && unix_path"`
Paths []string `form:"paths" json:"paths" validate:"required && dive && required"`
File string `form:"file" json:"file" validate:"required && unix_path"`
}
type FileUnCompress struct {
File string `form:"file" json:"file" validate:"required|isUnixPath"`
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
File string `form:"file" json:"file" validate:"required && unix_path"`
Path string `form:"path" json:"path" validate:"required && unix_path"`
}
// ChunkUploadStart 分块上传开始请求
type ChunkUploadStart struct {
Path string `json:"path" validate:"required|isUnixPath"` // 目标目录
FileName string `json:"file_name" validate:"required"` // 文件名
FileHash string `json:"file_hash" validate:"required|len:64"` // 文件SHA256
ChunkCount int `json:"chunk_count" validate:"required|min:1"` // 分块总数
Force bool `json:"force"` // 是否覆盖已存在文件
Path string `json:"path" validate:"required && unix_path"` // 目标目录
FileName string `json:"file_name" validate:"required"` // 文件名
FileHash string `json:"file_hash" validate:"required && len:64"` // 文件SHA256
ChunkCount int `json:"chunk_count" validate:"required && min:1"` // 分块总数
Force bool `json:"force"` // 是否覆盖已存在文件
}
// ChunkUploadFinish 分块上传完成请求
type ChunkUploadFinish struct {
Path string `json:"path" validate:"required|isUnixPath"` // 目标目录
FileName string `json:"file_name" validate:"required"` // 文件名
FileHash string `json:"file_hash" validate:"required|len:64"` // 文件SHA256
ChunkCount int `json:"chunk_count" validate:"required|min:1"` // 分块总数
Force bool `json:"force"` // 是否覆盖已存在文件
Path string `json:"path" validate:"required && unix_path"` // 目标目录
FileName string `json:"file_name" validate:"required"` // 文件名
FileHash string `json:"file_hash" validate:"required && len:64"` // 文件SHA256
ChunkCount int `json:"chunk_count" validate:"required && min:1"` // 分块总数
Force bool `json:"force"` // 是否覆盖已存在文件
}
+17 -17
View File
@@ -5,27 +5,27 @@ type FirewallStatus struct {
}
type FirewallRule struct {
Type string `json:"type"`
Family string `json:"family" validate:"required|in:ipv4,ipv6"`
PortStart uint `json:"port_start" validate:"required|min:1|max:65535"`
PortEnd uint `json:"port_end" validate:"required|min:1|max:65535"`
Protocol string `json:"protocol" validate:"required|in:tcp,udp,tcp/udp"`
Address string `json:"address"`
Strategy string `json:"strategy" validate:"required|in:accept,drop,reject"`
Direction string `json:"direction" validate:"required|in:in,out"`
Type string `json:"type" validate:"in:rich,normal"`
Family string `json:"family" validate:"required && in:ipv4,ipv6"`
PortStart uint `json:"port_start" validate:"required && min:1 && max:65535"`
PortEnd uint `json:"port_end" validate:"required && min:1 && max:65535 && gtefield:PortStart"`
Protocol string `json:"protocol" validate:"required && in:tcp,udp,tcp/udp"`
Address string `json:"address" validate:"ipcidr"`
Strategy string `json:"strategy" validate:"required && in:accept,drop,reject"`
Direction string `json:"direction" validate:"required && in:in,out"`
}
type FirewallIPRule struct {
Family string `json:"family" validate:"required|in:ipv4,ipv6"`
Protocol string `json:"protocol" validate:"required|in:tcp,udp,tcp/udp"`
Address string `json:"address"`
Strategy string `json:"strategy" validate:"required|in:accept,drop,reject"`
Direction string `json:"direction" validate:"required|in:in,out"`
Family string `json:"family" validate:"required && in:ipv4,ipv6"`
Protocol string `json:"protocol" validate:"required && in:tcp,udp,tcp/udp"`
Address string `json:"address" validate:"ipcidr"`
Strategy string `json:"strategy" validate:"required && in:accept,drop,reject"`
Direction string `json:"direction" validate:"required && in:in,out"`
}
type FirewallForward struct {
Protocol string `json:"protocol" validate:"required|in:tcp,udp,tcp/udp"`
Port uint `json:"port" validate:"required|min:1|max:65535"`
TargetIP string `json:"target_ip" validate:"required"`
TargetPort uint `json:"target_port" validate:"required|min:1|max:65535"`
Protocol string `json:"protocol" validate:"required && in:tcp,udp,tcp/udp"`
Port uint `json:"port" validate:"required && min:1 && max:65535"`
TargetIP string `json:"target_ip" validate:"required && ip"`
TargetPort uint `json:"target_port" validate:"required && min:1 && max:65535"`
}
+4 -4
View File
@@ -3,11 +3,11 @@ package request
// FirewallScanSetting 扫描感知设置
type FirewallScanSetting struct {
Enabled bool `json:"enabled"`
Days uint `json:"days" validate:"min:1|max:365"`
Days uint `json:"days" validate:"min:1 && max:365"`
Interfaces []string `json:"interfaces"`
AutoBlock bool `json:"auto_block"`
BlockThreshold uint `json:"block_threshold" validate:"min:1|max:100000"`
BlockWindow uint `json:"block_window" validate:"min:1|max:1440"`
BlockThreshold uint `json:"block_threshold" validate:"min:1 && max:100000"`
BlockWindow uint `json:"block_window" validate:"min:1 && max:1440"`
BlockDuration uint `json:"block_duration" validate:"max:87600"`
Whitelist []string `json:"whitelist"`
Whitelist []string `json:"whitelist" validate:"dive && ipcidr"`
}
+4 -4
View File
@@ -2,12 +2,12 @@ package request
// LogList 日志列表请求
type LogList struct {
Type string `json:"type" form:"type" query:"type" validate:"required|in:app,db,http"`
Limit int `json:"limit" form:"limit" query:"limit" validate:"min:1|max:1000"`
Date string `json:"date" form:"date" query:"date"` // 日期,格式为 YYYY-MM-DD,空表示当天
Type string `json:"type" form:"type" query:"type" validate:"required && in:app,db,http"`
Limit int `json:"limit" form:"limit" query:"limit" validate:"min:1 && max:1000"`
Date string `json:"date" form:"date" query:"date" validate:"datetime:2006-01-02"` // 日期,格式为 YYYY-MM-DD,空表示当天
}
// LogDates 日志日期列表请求
type LogDates struct {
Type string `json:"type" form:"type" query:"type" validate:"required|in:app,db,http"`
Type string `json:"type" form:"type" query:"type" validate:"required && in:app,db,http"`
}
+1 -1
View File
@@ -3,7 +3,7 @@ package request
type MonitorSetting struct {
Enabled bool `json:"enabled"`
Days uint `json:"days"`
Interval uint `json:"interval" validate:"required|min:1|max:120"` // 采集间隔(分钟),最小 1
Interval uint `json:"interval" validate:"required && min:1 && max:120"` // 采集间隔(分钟),最小 1
}
type MonitorList struct {
+2 -2
View File
@@ -5,8 +5,8 @@ import (
)
type Paginate struct {
Page uint `json:"page" form:"page" query:"page" validate:"required|min:1"`
Limit uint `json:"limit" form:"limit" query:"limit" validate:"required|min:1|max:10000"`
Page uint `json:"page" form:"page" query:"page" validate:"required && min:1"`
Limit uint `json:"limit" form:"limit" query:"limit" validate:"required && min:1 && max:10000"`
}
func (r *Paginate) Messages(_ *http.Request) map[string]string {
+1 -1
View File
@@ -14,7 +14,7 @@ type ProcessDetail struct {
// 支持的信号: SIGHUP(1), SIGINT(2), SIGKILL(9), SIGUSR1(10), SIGUSR2(12), SIGTERM(15), SIGCONT(18), SIGSTOP(19)
type ProcessSignal struct {
PID int32 `json:"pid" validate:"required"`
Signal int `json:"signal" validate:"required|in:1,2,9,10,12,15,18,19"`
Signal int `json:"signal" validate:"required && in:1,2,9,10,12,15,18,19"`
}
// ProcessList 进程列表请求
+8 -8
View File
@@ -3,8 +3,8 @@ package request
import "github.com/acepanel/panel/v3/pkg/types"
type ProjectCreate struct {
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Type types.ProjectType `form:"type" json:"type" validate:"required|in:general,php,java,go,python,nodejs,dotnet"`
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
Type types.ProjectType `form:"type" json:"type" validate:"required && in:general,php,java,go,python,nodejs,dotnet"`
Description string `form:"description" json:"description"`
RootDir string `form:"root_dir" json:"root_dir"`
WorkingDir string `form:"working_dir" json:"working_dir"`
@@ -15,8 +15,8 @@ type ProjectCreate struct {
}
type ProjectUpdate struct {
ID uint `form:"id" json:"id" validate:"required|exists:projects,id"`
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
ID uint `form:"id" json:"id" validate:"required && exists:projects,id"`
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
Description string `form:"description" json:"description"`
RootDir string `form:"root_dir" json:"root_dir" validate:"required"`
WorkingDir string `form:"working_dir" json:"working_dir"`
@@ -28,9 +28,9 @@ type ProjectUpdate struct {
User string `form:"user" json:"user"`
Restart string `json:"restart"`
RestartSec string `json:"restart_sec"`
RestartMax int `json:"restart_max"`
TimeoutStartSec int `json:"timeout_start_sec"`
TimeoutStopSec int `json:"timeout_stop_sec"`
RestartMax int `json:"restart_max" validate:"min:0"`
TimeoutStartSec int `json:"timeout_start_sec" validate:"min:0"`
TimeoutStopSec int `json:"timeout_stop_sec" validate:"min:0"`
Environments []types.KV `form:"environments" json:"environments"`
StandardOutput string `form:"standard_output" json:"standard_output"`
StandardError string `form:"standard_error" json:"standard_error"`
@@ -39,7 +39,7 @@ type ProjectUpdate struct {
After []string `form:"after" json:"after"`
Before []string `form:"before" json:"before"`
MemoryLimit float64 `form:"memory_limit" json:"memory_limit"`
MemoryLimit float64 `form:"memory_limit" json:"memory_limit" validate:"min:0"`
CPUQuota string `form:"cpu_quota" json:"cpu_quota"`
NoNewPrivileges bool `form:"no_new_privileges" json:"no_new_privileges"`
+1 -1
View File
@@ -1,7 +1,7 @@
package request
type SafeUpdateSSH struct {
Port uint `json:"port" form:"port" validate:"required|min:1|max:65535"`
Port uint `json:"port" form:"port" validate:"required && min:1 && max:65535"`
Status bool `json:"status" form:"status"`
}
+10 -18
View File
@@ -1,10 +1,8 @@
package request
import "net/http"
type SettingPanel struct {
Name string `json:"name" validate:"required"`
Channel string `json:"channel" validate:"required|in:stable,beta"`
Channel string `json:"channel" validate:"required && in:stable,beta"`
Locale string `json:"locale" validate:"required"`
Entrance string `json:"entrance" validate:"required"`
EntranceError string `json:"entrance_error" validate:"in:418,nginx,close"` // 安全入口错误页伪装类型
@@ -12,34 +10,28 @@ type SettingPanel struct {
OfflineMode bool `json:"offline_mode"`
AutoUpdate bool `json:"auto_update"`
TwoFA bool `json:"two_fa"`
Lifetime uint `json:"lifetime" validate:"required|min:10|max:43200"` // 登录超时,单位:分
Lifetime uint `json:"lifetime" validate:"required && min:10 && max:43200"` // 登录超时,单位:分
IPHeader string `json:"ip_header"`
BindDomain []string `json:"bind_domain"`
BindIP []string `json:"bind_ip"`
BindIP []string `json:"bind_ip" validate:"dive && ipcidr"`
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"` // 隐藏的菜单项
CustomLogo string `json:"custom_logo" validate:"isFullURL"` // 自定义 Logo URL
IPDBType string `json:"ipdb_type"` // IPDB 来源类型: "" / "custom" / "subscribe"
IPDBURL string `json:"ipdb_url"` // IPDB 订阅链接
IPDBPath string `json:"ipdb_path"` // IPDB 地理位置库路径
Port uint `json:"port" validate:"required|min:1|max:65535"` // 面板端口
TLS string `json:"tls"` // 面板 TLS: off, acme, self-signed, custom
HiddenMenu []string `json:"hidden_menu"` // 隐藏的菜单项
CustomLogo string `json:"custom_logo" validate:"url"` // 自定义 Logo URL
IPDBType string `json:"ipdb_type" validate:"in:custom,subscribe"` // IPDB 来源类型: "" / "custom" / "subscribe"
IPDBURL string `json:"ipdb_url"` // IPDB 订阅链接
IPDBPath string `json:"ipdb_path"` // IPDB 地理位置库路径
Port uint `json:"port" validate:"required && min:1 && max:65535"` // 面板端口
TLS string `json:"tls" validate:"in:off,acme,self-signed,custom"` // 面板 TLS: off, acme, self-signed, custom
PublicIP []string `json:"public_ip"`
Cert string `json:"cert"`
Key string `json:"key"`
}
func (r *SettingPanel) Rules(_ *http.Request) map[string]string {
return map[string]string{
"BindIP.*": "ipcidr",
}
}
type SettingCert struct {
Cert string `json:"cert" validate:"required"`
Key string `json:"key" validate:"required"`
+9 -9
View File
@@ -3,24 +3,24 @@ package request
type SSHCreate struct {
Name string `json:"name" form:"name"`
Host string `json:"host" form:"host" validate:"required"`
Port uint `json:"port" form:"port" validate:"required|min:1|max:65535"`
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required|in:password,publickey"`
Port uint `json:"port" form:"port" validate:"required && min:1 && max:65535"`
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required && in:password,publickey"`
User string `json:"user" form:"user" validate:"required"`
Password string `json:"password" form:"password" validate:"requiredIf:AuthMethod,password"`
Key string `json:"key" form:"key" validate:"requiredIf:AuthMethod,publickey"`
Password string `json:"password" form:"password" validate:"required_if:AuthMethod,password"`
Key string `json:"key" form:"key" validate:"required_if:AuthMethod,publickey"`
Passphrase string `json:"passphrase" form:"passphrase"`
Remark string `json:"remark" form:"remark"`
}
type SSHUpdate struct {
ID uint `form:"id" json:"id" validate:"required|exists:sshes,id"`
ID uint `form:"id" json:"id" validate:"required && exists:sshes,id"`
Name string `json:"name" form:"name"`
Host string `json:"host" form:"host" validate:"required"`
Port uint `json:"port" form:"port" validate:"required|min:1|max:65535"`
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required|in:password,publickey"`
Port uint `json:"port" form:"port" validate:"required && min:1 && max:65535"`
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required && in:password,publickey"`
User string `json:"user" form:"user" validate:"required"`
Password string `json:"password" form:"password" validate:"requiredIf:AuthMethod,password"`
Key string `json:"key" form:"key" validate:"requiredIf:AuthMethod,publickey"`
Password string `json:"password" form:"password" validate:"required_if:AuthMethod,password"`
Key string `json:"key" form:"key" validate:"required_if:AuthMethod,publickey"`
Passphrase string `json:"passphrase" form:"passphrase"`
Remark string `json:"remark" form:"remark"`
}
+1 -1
View File
@@ -8,7 +8,7 @@ type TemplateSlug struct {
type TemplateCreate struct {
Slug string `json:"slug" validate:"required"`
Name string `json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
Name string `json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
Compose string `json:"compose"`
Envs []types.KV `json:"envs"`
AutoFirewall bool `json:"auto_firewall"`
+1 -1
View File
@@ -1,5 +1,5 @@
package request
type ToolboxBenchmarkTest struct {
Name string `json:"name" validate:"required|in:image,machine,compile,encryption,compression,physics,json,memory,disk"`
Name string `json:"name" validate:"required && in:image,machine,compile,encryption,compression,physics,json,memory,disk"`
}
+8 -8
View File
@@ -8,20 +8,20 @@ type ToolboxDiskDevice struct {
// ToolboxDiskMount 挂载请求
type ToolboxDiskMount struct {
Device string `form:"device" json:"device" validate:"required"`
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
Path string `form:"path" json:"path" validate:"required && unix_path"`
WriteFstab bool `form:"write_fstab" json:"write_fstab"`
MountOption string `form:"mount_option" json:"mount_option"`
}
// ToolboxDiskUmount 卸载请求
type ToolboxDiskUmount struct {
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
Path string `form:"path" json:"path" validate:"required && unix_path"`
}
// ToolboxDiskFormat 格式化请求
type ToolboxDiskFormat struct {
Device string `form:"device" json:"device" validate:"required"`
FsType string `form:"fs_type" json:"fs_type" validate:"required|in:ext4,ext3,xfs,btrfs"`
FsType string `form:"fs_type" json:"fs_type" validate:"required && in:ext4,ext3,xfs,btrfs"`
}
// ToolboxDiskVG 卷组请求
@@ -34,7 +34,7 @@ type ToolboxDiskVG struct {
type ToolboxDiskLV struct {
Name string `form:"name" json:"name" validate:"required"`
VGName string `form:"vg_name" json:"vg_name" validate:"required"`
Size int `form:"size" json:"size" validate:"required|min:1"`
Size int `form:"size" json:"size" validate:"required && min:1"`
}
// ToolboxDiskVGName 卷组名称请求
@@ -44,20 +44,20 @@ type ToolboxDiskVGName struct {
// ToolboxDiskLVPath 逻辑卷路径请求
type ToolboxDiskLVPath struct {
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
Path string `form:"path" json:"path" validate:"required && unix_path"`
}
// ToolboxDiskExtendLV 扩容逻辑卷请求
type ToolboxDiskExtendLV struct {
Path string `form:"path" json:"path" validate:"required|isUnixPath"`
Size int `form:"size" json:"size" validate:"required|min:1"`
Path string `form:"path" json:"path" validate:"required && unix_path"`
Size int `form:"size" json:"size" validate:"required && min:1"`
Resize bool `form:"resize" json:"resize"`
}
// ToolboxDiskInit 初始化磁盘请求
type ToolboxDiskInit struct {
Device string `form:"device" json:"device" validate:"required"`
FsType string `form:"fs_type" json:"fs_type" validate:"required|in:ext4,ext3,xfs,btrfs"`
FsType string `form:"fs_type" json:"fs_type" validate:"required && in:ext4,ext3,xfs,btrfs"`
}
// ToolboxDiskFstabEntry fstab 条目结构
+1 -1
View File
@@ -2,5 +2,5 @@ package request
// ToolboxLogClean 日志清理请求
type ToolboxLogClean struct {
Type string `form:"type" json:"type" validate:"required|in:panel,website,mysql,postgresql,docker,system"`
Type string `form:"type" json:"type" validate:"required && in:panel,website,mysql,postgresql,docker,system"`
}
+3 -3
View File
@@ -25,7 +25,7 @@ type ToolboxMigrationWebsite struct {
// ToolboxMigrationDatabase 迁移数据库项
type ToolboxMigrationDatabase struct {
Type string `json:"type"` // mysql / postgresql
Type string `json:"type" validate:"in:mysql,postgresql"` // mysql / postgresql
Name string `json:"name"`
ServerID uint `json:"server_id"`
Server string `json:"server"` // 服务器名称
@@ -38,8 +38,8 @@ type ToolboxMigrationDatabaseUser struct {
Password string `json:"password"`
Host string `json:"host"` // 仅 mysql
ServerID uint `json:"server_id"`
Server string `json:"server"` // 服务器名称
Type string `json:"type"` // mysql / postgresql
Server string `json:"server"` // 服务器名称
Type string `json:"type" validate:"in:mysql,postgresql"` // mysql / postgresql
}
// ToolboxMigrationProject 迁移项目项
+3 -3
View File
@@ -2,7 +2,7 @@ package request
// ToolboxSSHPort SSH 端口设置
type ToolboxSSHPort struct {
Port uint `form:"port" json:"port" validate:"required|min:1|max:65535"`
Port uint `form:"port" json:"port" validate:"required && min:1 && max:65535"`
}
// ToolboxSSHPasswordAuth SSH 密码认证设置
@@ -17,10 +17,10 @@ type ToolboxSSHPubKeyAuth struct {
// ToolboxSSHRootLogin Root 登录设置
type ToolboxSSHRootLogin struct {
Mode string `form:"mode" json:"mode" validate:"required|in:yes,no,prohibit-password,forced-commands-only"`
Mode string `form:"mode" json:"mode" validate:"required && in:yes,no,prohibit-password,forced-commands-only"`
}
// ToolboxSSHRootPassword Root 密码设置
type ToolboxSSHRootPassword struct {
Password string `form:"password" json:"password" validate:"required|password"`
Password string `form:"password" json:"password" validate:"required && password"`
}
+4 -4
View File
@@ -3,8 +3,8 @@ package request
import "time"
type ToolboxSystemDNS struct {
DNS1 string `form:"dns1" json:"dns1" validate:"required"`
DNS2 string `form:"dns2" json:"dns2" validate:"required"`
DNS1 string `form:"dns1" json:"dns1" validate:"required && ip"`
DNS2 string `form:"dns2" json:"dns2" validate:"required && ip"`
}
type ToolboxSystemSWAP struct {
@@ -20,7 +20,7 @@ type ToolboxSystemTime struct {
}
type ToolboxSystemHostname struct {
Hostname string `form:"hostname" json:"hostname" validate:"required|regex:^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$"`
Hostname string `form:"hostname" json:"hostname" validate:"required && regex:\"^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$\""`
}
type ToolboxSystemHosts struct {
@@ -28,7 +28,7 @@ type ToolboxSystemHosts struct {
}
type ToolboxSystemPassword struct {
Password string `form:"password" json:"password" validate:"required|password"`
Password string `form:"password" json:"password" validate:"required && password"`
}
type ToolboxSystemSyncTime struct {
+11 -11
View File
@@ -1,7 +1,7 @@
package request
type UserID struct {
ID uint `json:"id" validate:"required|exists:users,id"`
ID uint `json:"id" validate:"required && exists:users,id"`
}
type UserLogin struct {
@@ -17,28 +17,28 @@ type UserIsTwoFA struct {
}
type UserCreate struct {
Username string `json:"username" validate:"required|notExists:users,username|regex:^[a-zA-Z0-9_-]+$"`
Password string `json:"password" validate:"required|password"`
Email string `json:"email" validate:"required|email"`
Username string `json:"username" validate:"required && not_exists:users,username && regex:\"^[a-zA-Z0-9_-]+$\""`
Password string `json:"password" validate:"required && password"`
Email string `json:"email" validate:"required && email"`
}
type UserUpdateUsername struct {
ID uint `json:"id" validate:"required|exists:users,id"`
Username string `json:"username" validate:"required|notExists:users,username|regex:^[a-zA-Z0-9_-]+$"`
ID uint `json:"id" validate:"required && exists:users,id"`
Username string `json:"username" validate:"required && not_exists:users,username && regex:\"^[a-zA-Z0-9_-]+$\""`
}
type UserUpdatePassword struct {
ID uint `json:"id" validate:"required|exists:users,id"`
Password string `json:"password" validate:"required|password"`
ID uint `json:"id" validate:"required && exists:users,id"`
Password string `json:"password" validate:"required && password"`
}
type UserUpdateEmail struct {
ID uint `json:"id" validate:"required|exists:users,id"`
Email string `json:"email" validate:"required|email"`
ID uint `json:"id" validate:"required && exists:users,id"`
Email string `json:"email" validate:"required && email"`
}
type UserUpdateTwoFA struct {
ID uint `uri:"id" validate:"required|exists:users,id"`
ID uint `uri:"id" validate:"required && exists:users,id"`
Secret string `json:"secret"`
Code string `json:"code"`
}
+1 -1
View File
@@ -5,6 +5,6 @@ type UserPasskeyList struct {
}
type UserPasskeyDelete struct {
ID uint `uri:"id" validate:"required|min:1"`
ID uint `uri:"id" validate:"required && min:1"`
UserID uint `query:"user_id"`
}
+3 -17
View File
@@ -1,32 +1,18 @@
package request
import "net/http"
type UserTokenList struct {
UserID uint `query:"user_id"`
Paginate
}
type UserTokenCreate struct {
UserID uint `json:"user_id" validate:"required|exists:users,id"`
IPs []string `json:"ips"`
UserID uint `json:"user_id" validate:"required && exists:users,id"`
IPs []string `json:"ips" validate:"dive && ipcidr"`
ExpiredAt int64 `json:"expired_at" validate:"required"`
}
func (r *UserTokenCreate) Rules(_ *http.Request) map[string]string {
return map[string]string{
"IPs.*": "ipcidr",
}
}
type UserTokenUpdate struct {
ID uint `uri:"id"`
IPs []string `json:"ips"`
IPs []string `json:"ips" validate:"dive && ipcidr"`
ExpiredAt int64 `json:"expired_at" validate:"required"`
}
func (r *UserTokenUpdate) Rules(_ *http.Request) map[string]string {
return map[string]string{
"IPs.*": "ipcidr",
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ type WebHookCreate struct {
}
type WebHookUpdate struct {
ID uint `json:"id" form:"id" uri:"id" validate:"required|exists:web_hooks,id"`
ID uint `json:"id" form:"id" uri:"id" validate:"required && exists:web_hooks,id"`
Name string `json:"name" form:"name" validate:"required"`
Script string `json:"script" form:"script" validate:"required"`
Raw bool `json:"raw" form:"raw"`
+27 -50
View File
@@ -1,8 +1,6 @@
package request
import (
"net/http"
"github.com/acepanel/panel/v3/pkg/webserver/types"
)
@@ -10,57 +8,44 @@ type WebsiteDefaultConfig struct {
Index string `json:"index" form:"index" validate:"required"`
Stop string `json:"stop" form:"stop" validate:"required"`
NotFound string `json:"not_found" form:"not_found"`
TLSVersions []string `json:"tls_versions" form:"tls_versions" validate:"required|isSlice"`
}
func (r *WebsiteDefaultConfig) Rules(_ *http.Request) map[string]string {
return map[string]string{
"TLSVersions.*": "required",
}
TLSVersions []string `json:"tls_versions" form:"tls_versions" validate:"required && dive && required"`
}
type WebsiteList struct {
Type string `json:"type" form:"type" validate:"required|in:all,proxy,static,php"`
Type string `json:"type" form:"type" validate:"required && in:all,proxy,static,php"`
Paginate
}
type WebsiteCreate struct {
Type string `json:"type" form:"type" validate:"required|in:proxy,static,php"`
Name string `form:"name" json:"name" validate:"required|notExists:websites,name|not_in:phpmyadmin,default|regex:^[a-zA-Z0-9_-]+$"`
Listens []string `form:"listens" json:"listens" validate:"required|isSlice"`
Domains []string `form:"domains" json:"domains" validate:"required|isSlice"`
Type string `json:"type" form:"type" validate:"required && in:proxy,static,php"`
Name string `form:"name" json:"name" validate:"required && not_exists:websites,name && not_in:phpmyadmin,default && regex:\"^[a-zA-Z0-9_-]+$\""`
Listens []string `form:"listens" json:"listens" validate:"required && dive && required"`
Domains []string `form:"domains" json:"domains" validate:"required && dive && required"`
Path string `form:"path" json:"path"`
DB bool `form:"db" json:"db"`
DBType string `form:"db_type" json:"db_type" validate:"requiredIf:DB,true"`
DBName string `form:"db_name" json:"db_name" validate:"requiredIf:DB,true|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
DBUser string `form:"db_user" json:"db_user" validate:"requiredIf:DB,true|regex:^[a-zA-Z_-][a-zA-Z0-9_-]*$"`
DBPassword string `form:"db_password" json:"db_password" validate:"requiredIf:DB,true"`
DBType string `form:"db_type" json:"db_type" validate:"required_if:DB,true"`
DBName string `form:"db_name" json:"db_name" validate:"required_if:DB,true && regex:\"^[a-zA-Z_-][a-zA-Z0-9_-]*$\""`
DBUser string `form:"db_user" json:"db_user" validate:"required_if:DB,true && regex:\"^[a-zA-Z_-][a-zA-Z0-9_-]*$\""`
DBPassword string `form:"db_password" json:"db_password" validate:"required_if:DB,true"`
Remark string `form:"remark" json:"remark"`
PHP uint `form:"php" json:"php" validate:"requiredIf:Type,php"` // 仅 PHP 网站需要
Proxy string `form:"proxy" json:"proxy" validate:"requiredIf:Type,proxy"` // 仅反向代理网站需要
}
func (r *WebsiteCreate) Rules(_ *http.Request) map[string]string {
return map[string]string{
"Listens.*": "required",
"Domains.*": "required",
}
PHP uint `form:"php" json:"php" validate:"required_if:Type,php"` // 仅 PHP 网站需要
Proxy string `form:"proxy" json:"proxy" validate:"required_if:Type,proxy"` // 仅反向代理网站需要
}
type WebsiteDelete struct {
ID uint `form:"id" json:"id" validate:"required|exists:websites,id"`
ID uint `form:"id" json:"id" validate:"required && exists:websites,id"`
Path bool `form:"path" json:"path"`
DB bool `form:"db" json:"db"`
}
type WebsiteUpdate struct {
ID uint `form:"id" json:"id" validate:"required|exists:websites,id"`
Listens []types.Listen `form:"listens" json:"listens" validate:"required|isSlice"`
Domains []string `form:"domains" json:"domains" validate:"required|isSlice"`
Path string `form:"path" json:"path" validate:"required"` // 网站目录
Root string `form:"root" json:"root" validate:"required"` // 运行目录
Index []string `form:"index" json:"index" validate:"required|isSlice"`
ID uint `form:"id" json:"id" validate:"required && exists:websites,id"`
Listens []types.Listen `form:"listens" json:"listens" validate:"required && dive && required"`
Domains []string `form:"domains" json:"domains" validate:"required && dive && required"`
Path string `form:"path" json:"path" validate:"required && unix_path"` // 网站目录
Root string `form:"root" json:"root" validate:"required && unix_path"` // 运行目录
Index []string `form:"index" json:"index" validate:"required && dive && required"`
// SSL 相关
SSL bool `form:"ssl" json:"ssl"`
@@ -95,43 +80,35 @@ type WebsiteUpdate struct {
CustomConfigs []WebsiteCustomConfig `json:"custom_configs"`
}
func (r *WebsiteUpdate) Rules(_ *http.Request) map[string]string {
return map[string]string{
"Listens.*": "required",
"Domains.*": "required",
"Index.*": "required",
}
}
// WebsiteCustomConfig 网站自定义配置请求
type WebsiteCustomConfig struct {
Name string `json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 配置名称
Scope string `json:"scope" validate:"required|in:site,shared"` // 作用域: site(此网站), shared(全局)
Content string `json:"content"` // 配置内容
Name string `json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""` // 配置名称
Scope string `json:"scope" validate:"required && in:site,shared"` // 作用域: site(此网站), shared(全局)
Content string `json:"content"` // 配置内容
}
type WebsiteUpdateRemark struct {
ID uint `form:"id" json:"id" validate:"required|exists:websites,id"`
ID uint `form:"id" json:"id" validate:"required && exists:websites,id"`
Remark string `form:"remark" json:"remark"`
}
type WebsiteUpdateStatus struct {
ID uint `json:"id" form:"id" validate:"required|exists:websites,id"`
ID uint `json:"id" form:"id" validate:"required && exists:websites,id"`
Status bool `json:"status" form:"status"`
}
type WebsiteUpdateExpireAt struct {
ID uint `json:"id" form:"id" validate:"required|exists:websites,id"`
ID uint `json:"id" form:"id" validate:"required && exists:websites,id"`
ExpireAt string `json:"expire_at" form:"expire_at"` // 为空表示清除到期时间(不限时)
}
type WebsiteUpdateCert struct {
Name string `json:"name" validate:"required|exists:websites,name|regex:^[a-zA-Z0-9_-]+$"`
Name string `json:"name" validate:"required && exists:websites,name && regex:\"^[a-zA-Z0-9_-]+$\""`
Cert string `json:"cert" validate:"required"`
Key string `json:"key" validate:"required"`
}
type WebsiteObtainCert struct {
ID uint `json:"id" form:"id" uri:"id" validate:"required|exists:websites,id"`
ID uint `json:"id" form:"id" uri:"id" validate:"required && exists:websites,id"`
DNSID uint `json:"dns_id" form:"dns_id"`
}
+7 -7
View File
@@ -10,18 +10,18 @@ import (
// WebsiteStatSetting 网站统计设置
type WebsiteStatSetting struct {
Days uint `json:"days" validate:"required|min:1|max:365"`
ErrBufMax int `json:"err_buf_max" validate:"min:0|max:1000000"`
UVMaxKeys int `json:"uv_max_keys" validate:"min:0|max:100000000"`
IPMaxKeys int `json:"ip_max_keys" validate:"min:0|max:100000000"`
DetailMaxKeys int `json:"detail_max_keys" validate:"min:0|max:100000000"`
Days uint `json:"days" validate:"required && min:1 && max:365"`
ErrBufMax int `json:"err_buf_max" validate:"min:0 && max:1000000"`
UVMaxKeys int `json:"uv_max_keys" validate:"min:0 && max:100000000"`
IPMaxKeys int `json:"ip_max_keys" validate:"min:0 && max:100000000"`
DetailMaxKeys int `json:"detail_max_keys" validate:"min:0 && max:100000000"`
BodyEnabled bool `json:"body_enabled"`
}
// WebsiteStatDateRange 统计日期范围查询参数
type WebsiteStatDateRange struct {
Start string `json:"start" form:"start" query:"start"`
End string `json:"end" form:"end" query:"end"`
Start string `json:"start" form:"start" query:"start" validate:"datetime:2006-01-02"`
End string `json:"end" form:"end" query:"end" validate:"datetime:2006-01-02"`
Sites string `json:"sites" form:"sites" query:"sites"`
}
+10 -2
View File
@@ -3,6 +3,7 @@ package rule
import (
"regexp"
"github.com/libtnb/validator"
"github.com/spf13/cast"
)
@@ -17,6 +18,13 @@ func NewCron() *Cron {
}
}
func (s *Cron) Passes(val any, options ...any) bool {
return s.re.MatchString(cast.ToString(val))
func (s *Cron) Signature() string { return "cron" }
func (s *Cron) Message() string { return "{field} must be a valid cron expression" }
func (s *Cron) Passes(f validator.Field) bool {
if validator.IsEmptyValue(f.Val()) {
return true
}
return s.re.MatchString(cast.ToString(f.Val().Interface()))
}
+17 -10
View File
@@ -3,15 +3,13 @@ package rule
import (
"fmt"
"github.com/libtnb/validator"
"gorm.io/gorm"
)
// Exists 验证一个值在某个表中的字段中存在,支持同时判断多个字段
// Exists verify a value exists in a table field, support judging multiple fields at the same time
// 用法:exists:表名称,字段名称,字段名称,字段名称
// Usage: exists:table_name,field_name,field_name,field_name
// 用法:exists:表名称,字段名称,字段名称
// 例子:exists:users,phone,email
// Example: exists:users,phone,email
type Exists struct {
db *gorm.DB
}
@@ -20,13 +18,23 @@ func NewExists(db *gorm.DB) *Exists {
return &Exists{db: db}
}
func (r *Exists) Passes(val any, options ...any) bool {
if len(options) < 2 {
func (r *Exists) Signature() string { return "exists" }
func (r *Exists) Message() string { return "{field} is not exists" }
func (r *Exists) Passes(f validator.Field) bool {
rv := f.Val()
if validator.IsEmptyValue(rv) {
return true
}
args := f.Attrs()
if len(args) < 2 {
return false
}
tableName := options[0].(string)
fieldNames := options[1:]
val := rv.Interface()
tableName := args[0]
fieldNames := args[1:]
query := r.db.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), val)
for _, fieldName := range fieldNames[1:] {
@@ -34,8 +42,7 @@ func (r *Exists) Passes(val any, options ...any) bool {
}
var count int64
err := query.Count(&count).Error
if err != nil {
if err := query.Count(&count).Error; err != nil {
return false
}
+22 -11
View File
@@ -1,22 +1,33 @@
package rule
import "net"
import (
"net"
// IPCIDR 验证一个值是否是一个有效的 IP 或 CIDR 格式
"github.com/libtnb/validator"
"github.com/spf13/cast"
)
// IPCIDR 验证一个值是否是有效的 IP 或 CIDR
type IPCIDR struct{}
func NewIPCIDR() *IPCIDR {
return &IPCIDR{}
}
func (r *IPCIDR) Passes(val any, options ...any) bool {
if str, ok := val.(string); ok {
if ip := net.ParseIP(str); ip != nil {
return true // 是有效的 IP
}
if _, _, err := net.ParseCIDR(str); err == nil {
return true // 是有效的 CIDR
}
func (r *IPCIDR) Signature() string { return "ipcidr" }
func (r *IPCIDR) Message() string { return "{field} must be a valid IP address or CIDR notation" }
func (r *IPCIDR) Passes(f validator.Field) bool {
if validator.IsEmptyValue(f.Val()) {
return true
}
return false // 既不是 IP 也不是 CIDR
str := cast.ToString(f.Val().Interface())
if net.ParseIP(str) != nil {
return true
}
if _, _, err := net.ParseCIDR(str); err == nil {
return true
}
return false
}
+18 -11
View File
@@ -3,15 +3,13 @@ package rule
import (
"fmt"
"github.com/libtnb/validator"
"gorm.io/gorm"
)
// NotExists 验证一个值在某个表中的字段中不存在,支持同时判断多个字段
// NotExists verify a value does not exist in a table field, support judging multiple fields at the same time
// 用法notExists:表名称,字段名称,字段名称,字段名称
// Usage: notExists:table_name,field_name,field_name,field_name
// 例子:notExists:users,phone,email
// Example: notExists:users,phone,email
// 用法:not_exists:表名称,字段名称,字段名称
// 例子not_exists:users,phone,email
type NotExists struct {
db *gorm.DB
}
@@ -20,13 +18,23 @@ func NewNotExists(db *gorm.DB) *NotExists {
return &NotExists{db: db}
}
func (r *NotExists) Passes(val any, options ...any) bool {
if len(options) < 2 {
func (r *NotExists) Signature() string { return "not_exists" }
func (r *NotExists) Message() string { return "{field} is exists" }
func (r *NotExists) Passes(f validator.Field) bool {
rv := f.Val()
if validator.IsEmptyValue(rv) {
return true
}
args := f.Attrs()
if len(args) < 2 {
return false
}
tableName := options[0].(string)
fieldNames := options[1:]
val := rv.Interface()
tableName := args[0]
fieldNames := args[1:]
query := r.db.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), val)
for _, fieldName := range fieldNames[1:] {
@@ -34,8 +42,7 @@ func (r *NotExists) Passes(val any, options ...any) bool {
}
var count int64
err := query.Count(&count).Error
if err != nil {
if err := query.Count(&count).Error; err != nil {
return false
}

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