Compare commits

...

502 Commits

Author SHA1 Message Date
耗子 a997d548f7 feat: 优化菜单排序 2025-05-19 02:15:15 +08:00
耗子 aab5cf7e94 l10n: update translations (#754)
* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot
2025-05-19 02:03:49 +08:00
devhaozi 382bf4daaa chore(l10n): update backend pot files 2025-05-18 17:59:56 +00:00
耗子 3e048a8683 Merge remote-tracking branch 'origin/main' 2025-05-19 01:59:23 +08:00
耗子 7fa7bb4bd0 fix: lint 2025-05-19 01:59:18 +08:00
devhaozi d41ddb0ad0 chore(l10n): update backend pot files 2025-05-18 17:54:41 +00:00
耗子 cdbadcbb65 fix: lint 2025-05-19 01:54:12 +08:00
devhaozi b14d63c54c chore(l10n): update frontend pot files 2025-05-18 17:49:24 +00:00
耗子 3d47855cc1 feat: 优化跑分算法 2025-05-19 01:48:48 +08:00
devhaozi 819db43aaa chore(l10n): update frontend pot files 2025-05-18 16:35:34 +00:00
devhaozi de859afbe7 chore(l10n): update backend pot files 2025-05-18 16:35:28 +00:00
耗子 066c840efe feat: 移动工具箱和跑分为独立应用 2025-05-19 00:34:53 +08:00
耗子 bc4fcfe6ed feat: 移除工具箱和跑分应用 2025-05-19 00:09:29 +08:00
devhaozi d43090b84f chore(l10n): update backend pot files 2025-05-18 14:45:49 +00:00
耗子 919c37d1f9 fix: 两步验证算法错误 2025-05-18 22:44:43 +08:00
耗子 319b689d79 Merge remote-tracking branch 'origin/main' 2025-05-17 21:07:19 +08:00
耗子 9a6badd46c feat: 优化日志清理使用cat /dev/null 2025-05-17 21:07:14 +08:00
devhaozi ee94b01c2d chore(l10n): update backend pot files 2025-05-17 13:04:36 +00:00
耗子 7b471967a2 feat: 移除日志切割后自动重载nginx 2025-05-17 21:04:05 +08:00
devhaozi 6e039a7fad chore(l10n): update backend pot files 2025-05-17 11:03:26 +00:00
耗子 2f56606789 feat: 用户信息取首用户 2025-05-17 19:02:52 +08:00
耗子 a3210c0a92 feat: 证书账户表单默认使用letsencrypt 2025-05-17 19:01:16 +08:00
耗子 9876821b9a l10n: update translations (#750)
* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot
2025-05-17 18:34:58 +08:00
devhaozi ee7f846151 chore(l10n): update frontend pot files 2025-05-17 10:23:18 +00:00
耗子 6b1478594a fix: 服务禁用自启动提示错误 2025-05-17 18:22:43 +08:00
devhaozi 6448e7c5c6 chore(l10n): update frontend pot files 2025-05-17 10:21:56 +00:00
耗子 68c088ca03 Merge remote-tracking branch 'origin/main' 2025-05-17 18:21:18 +08:00
耗子 c5ec454b26 feat: 优化服务状态显示 2025-05-17 18:21:08 +08:00
devhaozi 12f9820ec5 chore(l10n): update backend pot files 2025-05-17 08:53:44 +00:00
耗子 d5d941c62b Merge remote-tracking branch 'origin/main' 2025-05-17 16:53:15 +08:00
耗子 e5f26d2f23 feat: 禁止通过api访问ws接口 2025-05-17 16:53:10 +08:00
耗子 433f3f99f6 fix: google eab获取 2025-05-16 10:53:54 +08:00
devhaozi b866f01985 chore(l10n): update backend pot files 2025-05-16 00:39:18 +00:00
耗子 07ba6624bf feat: 优化日志切割 2025-05-16 08:38:37 +08:00
耗子 855c8cfb51 New translations frontend.pot (Chinese Simplified) (#748)
Update translations (Chinese Simplified) frontend.pot
2025-05-16 00:52:46 +08:00
耗子 70175d309e refactor: 接口信息字段调整为msg 2025-05-16 00:35:48 +08:00
耗子 04d286ca03 refactor: 接口信息字段调整为msg 2025-05-16 00:29:00 +08:00
耗子 1a821b361f l10n: update translations (#747)
* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot
2025-05-15 23:21:00 +08:00
耗子 ec101ba9f7 l10n: update translations (#746)
* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot
2025-05-15 20:39:41 +08:00
devhaozi 830fa5aa86 chore(l10n): update frontend pot files 2025-05-15 12:11:18 +00:00
devhaozi aed6bb7856 chore(l10n): update backend pot files 2025-05-15 12:11:04 +00:00
耗子 7df9b92c58 feat: 添加cli命令及前端优化 2025-05-15 20:10:31 +08:00
耗子 9faa2a74df Merge remote-tracking branch 'origin/main' 2025-05-15 19:21:55 +08:00
耗子 94497f226f feat: 优化网站证书更新 2025-05-15 19:21:48 +08:00
devhaozi cff90ef444 chore(l10n): update backend pot files 2025-05-15 10:37:52 +00:00
耗子 f8ae9136c0 Merge remote-tracking branch 'origin/main' 2025-05-15 18:37:15 +08:00
耗子 7716866849 feat: 优化签名错误提示 2025-05-15 18:37:09 +08:00
devhaozi c690353f3e chore(l10n): update frontend pot files 2025-05-15 09:57:31 +00:00
耗子 373827f8e5 feat: 优化图标 2025-05-15 17:56:54 +08:00
devhaozi 587436781d chore(l10n): update backend pot files 2025-05-15 01:49:45 +00:00
耗子 069353d617 Merge remote-tracking branch 'origin/main' 2025-05-15 09:49:05 +08:00
耗子 7f87030cc8 feat: 传递channel参数 2025-05-15 09:47:43 +08:00
devhaozi 9064e41775 chore(l10n): update backend pot files 2025-05-15 01:08:51 +00:00
耗子 04c8eb5cad Merge remote-tracking branch 'origin/main' 2025-05-15 09:08:12 +08:00
耗子 867c14349e feat: 优化s3fs挂载获取 2025-05-15 09:08:03 +08:00
devhaozi a59fe6fe16 chore(l10n): update backend pot files 2025-05-15 00:30:40 +00:00
耗子 71f518c6f9 feat: 初始化网站类型 2025-05-15 08:29:51 +08:00
devhaozi a7192efda0 chore: update mocks 2025-05-14 20:07:16 +00:00
devhaozi f14b606fda chore(l10n): update backend pot files 2025-05-14 20:06:53 +00:00
耗子 82e310542f Merge remote-tracking branch 'origin/main' 2025-05-15 04:06:25 +08:00
耗子 16f31120f5 feat: 添加用于更新证书的接口 2025-05-15 04:06:19 +08:00
devhaozi 05ffab5d54 chore(l10n): update frontend pot files 2025-05-14 19:40:32 +00:00
耗子 f39cceda18 feat: 优化2FA模态窗样式 2025-05-15 03:39:49 +08:00
耗子 ec8bf8fdad l10n: update translations (#743)
* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot
2025-05-15 03:26:07 +08:00
devhaozi c0f82356aa chore(l10n): update backend pot files 2025-05-14 19:22:01 +00:00
耗子 14c46eccca Merge remote-tracking branch 'origin/main' 2025-05-15 03:21:25 +08:00
耗子 354e8a1270 feat: 重构签名算法 2025-05-15 03:21:18 +08:00
devhaozi 924e69070a chore: update mocks 2025-05-14 19:20:06 +00:00
devhaozi 137624815c chore(l10n): update backend pot files 2025-05-14 19:19:53 +00:00
耗子 1adba2da49 feat: 重构签名算法 2025-05-15 03:19:16 +08:00
耗子 00c02e4b16 l10n: update translations (#741)
* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot
2025-05-15 02:55:36 +08:00
devhaozi 813dcaedaf chore(l10n): update frontend pot files 2025-05-14 18:34:11 +00:00
devhaozi 0ef8bddf98 chore(l10n): update backend pot files 2025-05-14 18:33:09 +00:00
耗子 6276009025 fix: 翻译错误 2025-05-15 02:32:32 +08:00
devhaozi 2dc7096a2c chore: update mocks 2025-05-14 18:19:33 +00:00
devhaozi b84655512c chore(l10n): update backend pot files 2025-05-14 18:19:18 +00:00
devhaozi 271a2cad23 chore(l10n): update frontend pot files 2025-05-14 18:19:11 +00:00
耗子 a7dd9d411b feat: 支持创建access_token 2025-05-15 02:18:35 +08:00
devhaozi 103529a9b4 chore(l10n): update frontend pot files 2025-05-14 11:52:00 +00:00
耗子 ccef25314d Merge remote-tracking branch 'origin/main' 2025-05-14 19:51:29 +08:00
耗子 9f24e1e8a6 feat: 批量创建网站后触发表格刷新 2025-05-14 19:51:20 +08:00
devhaozi 7399e1632e chore(l10n): update frontend pot files 2025-05-14 11:44:34 +00:00
耗子 90715bcfaf Merge remote-tracking branch 'origin/main' 2025-05-14 19:43:54 +08:00
耗子 a64176e363 feat: 批量创建网站,close #129 2025-05-14 19:43:46 +08:00
devhaozi 910104d400 chore(l10n): update frontend pot files 2025-05-14 11:16:42 +00:00
devhaozi 9e87be88c7 chore(l10n): update backend pot files 2025-05-14 11:16:34 +00:00
耗子 fae5d136d4 feat: 登录支持输入2fa验证码 2025-05-14 19:16:06 +08:00
devhaozi 13f090e2b7 chore: update mocks 2025-05-14 11:04:54 +00:00
devhaozi 5896ba3c87 chore(l10n): update frontend pot files 2025-05-14 11:04:38 +00:00
devhaozi 65030866e5 chore(l10n): update backend pot files 2025-05-14 11:04:33 +00:00
耗子 5fd00acd48 feat: 用户支持开启2FA 2025-05-14 19:04:03 +08:00
耗子 462d6c0789 Merge remote-tracking branch 'origin/main' 2025-05-14 16:11:28 +08:00
耗子 c6b3b9e0c4 feat: l10n工作流不在pr运行 2025-05-14 16:11:21 +08:00
耗子 63b6349a74 l10n: update translations (#737)
* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot
2025-05-14 13:51:41 +08:00
devhaozi 781c5cf11e chore(l10n): update backend pot files 2025-05-13 20:21:58 +00:00
耗子 8697c893c4 Merge remote-tracking branch 'origin/main' 2025-05-14 04:21:22 +08:00
耗子 896e242827 feat: 2fa命令行 2025-05-14 04:21:17 +08:00
devhaozi b891d21c85 chore: update mocks 2025-05-13 20:00:17 +00:00
devhaozi 399b6f3537 chore(l10n): update frontend pot files 2025-05-13 20:00:02 +00:00
devhaozi cf8237b9c7 chore(l10n): update backend pot files 2025-05-13 19:59:54 +00:00
耗子 daccf651b6 feat: 2fa相关接口 2025-05-14 03:59:22 +08:00
devhaozi 4386334c31 chore: update mocks 2025-05-13 19:09:26 +00:00
devhaozi 0a445a4247 chore(l10n): update backend pot files 2025-05-13 19:09:15 +00:00
devhaozi 76cb8c46ba chore(l10n): update frontend pot files 2025-05-13 19:09:09 +00:00
耗子 d27a91570c feat: 支持绑定域名、IP、UA,close #670 2025-05-14 03:08:31 +08:00
devhaozi 554183caab chore: update mocks 2025-05-13 14:05:49 +00:00
耗子 275bba850a workflow: 优化提交前pull更改,防止工作流并发下无法提交 2025-05-13 22:05:02 +08:00
devhaozi 86c5239569 chore(l10n): update backend pot files 2025-05-13 13:47:20 +00:00
耗子 681f1e0805 Merge remote-tracking branch 'origin/main' 2025-05-13 21:46:52 +08:00
耗子 b767044199 feat: 为设置添加一个缓存层 2025-05-13 21:46:44 +08:00
devhaozi 67265ef969 chore(l10n): update backend pot files 2025-05-13 13:39:44 +00:00
耗子 8410b48ac0 feat: 将session超时写入主配置 2025-05-13 21:39:14 +08:00
devhaozi e6567e5e95 chore(l10n): update backend pot files 2025-05-12 18:43:35 +00:00
耗子 715b42dfbb feat: 添加API和更新渠道的设置字段 2025-05-13 02:42:55 +08:00
devhaozi 69ddca44bf chore(l10n): update backend pot files 2025-05-12 14:46:09 +00:00
耗子 71d9151fa0 feat: 添加2fa,登录超时,绑定域名、IP、UA的设置字段 2025-05-12 22:45:36 +08:00
耗子 3fd768a99c feat: 使用自建的iconify cdn,close #699 2025-05-12 21:37:13 +08:00
renovate[bot] c02eab6bfc chore(deps): Lock file maintenance (#733)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 05:49:08 +00:00
renovate[bot] 148e5946cf chore(deps): Update dependency @iconify/json to v2.2.337 (#732)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 18:37:08 +00:00
renovate[bot] 3590191ef9 chore(deps): Update dependency vite-plugin-static-copy to v3 (#730)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 16:30:13 +08:00
renovate[bot] b91c1a02f5 chore(deps): Update non-major dependencies (#731)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-11 06:30:44 +00:00
renovate[bot] d50ef7d62c chore(deps): Update dependency @vue-js-cron/naive-ui to v2.0.10 (#729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-10 17:19:33 +00:00
renovate[bot] 6ee4d67c2e chore(deps): Update dependency @vitejs/plugin-vue to v5.2.4 (#728)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-09 16:26:41 +00:00
renovate[bot] 3e325b13b4 chore(deps): Update non-major dependencies (#727)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-09 06:27:46 +00:00
renovate[bot] d6fb57f9ea chore(deps): Update non-major dependencies (#725)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-08 20:20:59 +00:00
renovate[bot] bffdf2821c chore(deps): Update module gorm.io/gorm to v1.26.1 (#723)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-08 11:21:29 +08:00
renovate[bot] c82d7bd48a chore(deps): Update non-major dependencies (#722)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-07 22:04:50 +00:00
renovate[bot] e4eb97e9dd chore(deps): Update non-major dependencies (#721)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 22:33:31 +00:00
renovate[bot] cbb168b3d5 chore(deps): Update non-major dependencies (#720)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 14:09:06 +08:00
renovate[bot] 60e817d9ea chore(deps): Update non-major dependencies (#719)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-06 14:04:19 +08:00
renovate[bot] e514d45486 chore(deps): Update dependency vite to v6.3.5 (#718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 16:14:22 +00:00
renovate[bot] 995d5524ea chore(deps): Update dependency npm-run-all2 to v8.0.1 (#717)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 13:45:29 +08:00
renovate[bot] b1d5276813 chore(deps): Update dependency npm-run-all2 to v8 (#710)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 13:33:57 +08:00
renovate[bot] 97ee651948 chore(deps): Lock file maintenance (#715)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 13:33:39 +08:00
renovate[bot] 5ddc25acc7 chore(deps): Update dependency @iconify/json to v2.2.335 (#716)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 13:18:52 +08:00
renovate[bot] 96ae14eefb chore(deps): Update golangci/golangci-lint-action action to v8 (#714)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 13:13:22 +08:00
renovate[bot] 820728861c chore(deps): Update non-major dependencies to v66.1.0 (#713)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-04 21:22:21 +00:00
renovate[bot] b476e9aec3 chore(deps): Update dependency eslint to v9.26.0 (#712)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-03 02:03:03 +00:00
renovate[bot] 5252854963 chore(deps): Update module github.com/spf13/cast to v1.8.0 (#709)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-03 02:04:43 +08:00
耗子 fe6e9efd09 refactor: 接口使用蛇形命名 2025-05-02 19:28:52 +08:00
renovate[bot] c0be351b95 chore(deps): Update dependency @iconify/json to v2.2.334 (#708)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-02 10:49:59 +00:00
renovate[bot] 573fc88c7a chore(deps): Update dependency cronstrue to v2.60.0 (#707)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-01 18:28:15 +00:00
renovate[bot] 9032450be8 chore(deps): Update dependency @iconify/vue to v5 (#706)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-01 20:04:06 +08:00
renovate[bot] bf0cdbe493 chore(deps): Update dependency eslint-plugin-vue to v10.1.0 (#705)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-30 16:05:16 +00:00
renovate[bot] 446d2410dc chore(deps): Update dependency vite to v6.3.4 (#704)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-30 14:42:36 +08:00
renovate[bot] 17febfa9e6 chore(deps): Update non-major dependencies (#703)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-30 02:06:31 +00:00
renovate[bot] 86eaef0ae3 chore(deps): Update dependency eslint-plugin-vue to v10.0.1 (#702)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-29 13:57:02 +08:00
renovate[bot] 874ecbc886 chore(deps): Update non-major dependencies (#701)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 22:33:51 +00:00
renovate[bot] 0c4b4b0a31 chore(deps): Update module github.com/urfave/cli/v3 to v3.3.2 (#700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 22:43:45 +08:00
renovate[bot] 5d8e773827 chore(deps): Update non-major dependencies (#698)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 22:39:32 +08:00
耗子 c9e1adc1e9 Merge remote-tracking branch 'origin/main' 2025-04-28 21:28:12 +08:00
耗子 27ce52ea13 chore: 暂时屏蔽github.com/libdns/gcore更新 2025-04-28 21:28:06 +08:00
renovate[bot] dea9702c36 chore(deps): Update module github.com/samber/lo to v1.50.0 (#696)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-27 02:25:44 +00:00
renovate[bot] 794fa29581 chore(deps): Update dependency @iconify/json to v2.2.332 (#694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-26 11:16:30 +00:00
renovate[bot] 30686bf5a1 chore(deps): Update non-major dependencies (#693)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-25 19:42:44 +00:00
耗子 ae9f3c4d15 fix: build 2025-04-25 14:31:31 +08:00
耗子 13fc5d2a5b fix: build 2025-04-25 14:30:29 +08:00
renovate[bot] 7bc2173edb chore(deps): Update non-major dependencies 2025-04-25 04:11:23 +00:00
renovate[bot] 5317e19d6e chore(deps): Lock file maintenance (#692)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 05:26:30 +00:00
renovate[bot] 0910b7c068 chore(deps): Update dependency eslint to v9.25.0 (#689)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-18 22:11:13 +00:00
devhaozi c800f6642b chore(l10n): update frontend pot files 2025-04-18 17:19:39 +00:00
耗子 1b80c85ebf feat: 优化压缩的文件名处理 2025-04-19 01:19:04 +08:00
耗子 714a33313e feat: 优化开发 2025-04-19 00:50:29 +08:00
耗子 a70b18b6f4 feat: 反代生成器添加X-Cache头 2025-04-19 00:10:08 +08:00
renovate[bot] 1a3d2cdd78 chore(deps): Update non-major dependencies (#688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-18 15:53:26 +08:00
renovate[bot] 45081b5535 chore(deps): Update non-major dependencies (#687)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 21:38:48 +00:00
耗子 600287c246 feat: 优化http挑战的流程,支持同时签多个域名 2025-04-17 01:26:13 +08:00
renovate[bot] a2607b4846 chore(deps): Update dependency vite-plugin-vue-devtools to v7.7.5 (#686)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-17 01:08:29 +08:00
耗子 13bb84f4f4 l10n: update translations (#684)
* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot
2025-04-17 00:46:59 +08:00
renovate[bot] e702f7813a chore(deps): Update module github.com/knadh/koanf/parsers/yaml to v1 (#682)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-16 21:37:42 +08:00
renovate[bot] ec8156b851 chore(deps): Update non-major dependencies (#685)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-16 21:37:24 +08:00
devhaozi 9da24a3238 chore(l10n): update frontend pot files 2025-04-16 07:31:21 +00:00
耗子 e390102a7c Merge remote-tracking branch 'origin/main' 2025-04-16 15:30:43 +08:00
耗子 2271084c02 fix: 修正网站启动停止的翻译 2025-04-16 15:30:35 +08:00
devhaozi dbb4a3514b chore(l10n): update frontend pot files 2025-04-16 06:20:31 +00:00
耗子 3eeefcdc9c feat: 证书默认邮箱域名改为yourdomain.com,因为example.com在lets黑名单里面 2025-04-16 14:19:52 +08:00
renovate[bot] c0be6addbc chore(deps): Update module github.com/mholt/acmez/v3 to v3.1.2 (#683)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-16 03:27:22 +00:00
renovate[bot] c344ade9fc chore(deps): Update non-major dependencies (#681)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-15 19:14:21 +00:00
耗子 6c2a3a26ab feat: 前端翻译仅在编译时打包 2025-04-15 18:06:56 +08:00
devhaozi 073f42a3bc chore(l10n): update frontend pot files 2025-04-15 09:55:13 +00:00
耗子 0e3007827e Merge remote-tracking branch 'origin/main' 2025-04-15 17:54:35 +08:00
耗子 8aa23cdd48 fix: 弹窗未显示错误信息 2025-04-15 17:54:30 +08:00
renovate[bot] 9765f7157d chore(deps): Update pnpm to v10.8.1 (#680)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 23:02:22 +00:00
耗子 ee3aabe3c8 Merge remote-tracking branch 'origin/main' 2025-04-14 19:02:41 +08:00
耗子 d39b3f66d6 feat: 容器日志只取最新100行 2025-04-14 19:02:35 +08:00
devhaozi cc26427417 chore(l10n): update frontend pot files 2025-04-14 10:03:45 +00:00
renovate[bot] 592af262da chore(deps): Update non-major dependencies (#679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 18:03:17 +08:00
renovate[bot] cf64103e36 chore(deps): Lock file maintenance (#678)
* chore(deps): Lock file maintenance

* chore(l10n): update frontend pot files

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 18:03:09 +08:00
耗子 044b3664ed feat: 优化nginx解析器错误返回 2025-04-14 15:46:11 +08:00
耗子 99eb46c2a9 l10n: update translations (#677)
* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* chore(l10n): update frontend pot files
2025-04-13 20:53:08 +08:00
devhaozi 2cae2fc807 chore(l10n): update frontend pot files 2025-04-13 12:26:23 +00:00
耗子 bfc0ff5c28 fix: 漏了一个翻译 2025-04-13 20:25:41 +08:00
devhaozi 504210cee1 chore(l10n): update frontend pot files 2025-04-13 11:48:07 +00:00
耗子 1d6d388d37 l10n: update translations (#676)
* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* chore(l10n): update frontend pot files
2025-04-13 19:47:30 +08:00
devhaozi 727ff7a52d chore(l10n): update backend pot files 2025-04-13 11:45:08 +00:00
耗子 8638dcb9c8 fix: php 拓展和负载页报错 2025-04-13 19:44:37 +08:00
耗子 d641c78000 l10n: update translations (#675)
* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* chore(l10n): update frontend pot files
2025-04-13 19:08:54 +08:00
耗子 49131d1d00 fix: 修复code-server服务名错误 2025-04-13 18:48:18 +08:00
devhaozi ccc43cd175 chore(l10n): update frontend pot files 2025-04-13 10:34:25 +00:00
耗子 0131f464cd l10n: update translations (#674)
* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* chore(l10n): update frontend pot files

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* chore(l10n): update frontend pot files

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* chore(l10n): update frontend pot files

* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot
2025-04-13 18:33:48 +08:00
devhaozi 752bc0a64e chore(l10n): update frontend pot files 2025-04-13 10:31:16 +00:00
耗子 fac981e1ee fix: lint 2025-04-13 18:30:47 +08:00
耗子 c6ce1a403d Merge remote-tracking branch 'origin/main' 2025-04-13 18:14:10 +08:00
耗子 79f7b4a510 fix: build 2025-04-13 18:14:05 +08:00
devhaozi 815418627f chore(l10n): update frontend pot files 2025-04-13 10:10:31 +00:00
耗子 d82e59b4eb feat: 新增code server应用,close #82 2025-04-13 18:09:55 +08:00
耗子 a6a42f0f11 Merge remote-tracking branch 'origin/main' 2025-04-13 17:00:50 +08:00
耗子 4d6b62ac18 feat: api支持传递locale,close #155 2025-04-13 17:00:43 +08:00
devhaozi 0547157c69 chore(l10n): update frontend pot files 2025-04-13 08:55:53 +00:00
耗子 493da1552d feat: 添加App翻译 2025-04-13 16:55:16 +08:00
devhaozi 0402427b71 chore(l10n): update frontend pot files 2025-04-13 07:49:53 +00:00
耗子 623dcbce1c fix: 菜单翻译无法被自动读取 2025-04-13 15:49:23 +08:00
耗子 98d9af89ec l10n: update translations (#672)
* New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot

* New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot

* chore(l10n): update frontend pot files
2025-04-13 15:21:15 +08:00
devhaozi b4546ec0ba chore(l10n): update frontend pot files 2025-04-12 22:18:13 +00:00
耗子 f140eb3f5f Merge remote-tracking branch 'origin/main' 2025-04-13 06:17:37 +08:00
耗子 432b5466e8 feat: 实现语言切换 2025-04-13 06:17:25 +08:00
devhaozi 402562ba93 chore(l10n): update frontend pot files 2025-04-12 21:04:03 +00:00
耗子 77bcf6e086 feat: 统一去掉按钮居中,不然显示效果太抽象了 2025-04-13 05:03:29 +08:00
devhaozi d9fee73e87 chore(l10n): update frontend pot files 2025-04-12 20:52:32 +00:00
耗子 e512187b60 Merge remote-tracking branch 'origin/main' 2025-04-13 04:52:04 +08:00
耗子 31a0617383 Merge remote-tracking branch 'origin/l10n'
# Conflicts:
#	web/src/locales/zh_CN.po
#	web/src/locales/zh_TW.po
2025-04-13 04:51:52 +08:00
耗子 98d80cd359 New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot
2025-04-13 04:50:49 +08:00
耗子 b0cae832c5 New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot
2025-04-13 04:46:11 +08:00
耗子 00b9a859f8 New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot
2025-04-13 04:46:10 +08:00
devhaozi 8b5a7ece43 chore(l10n): update frontend pot files 2025-04-12 20:45:57 +00:00
耗子 391b2a9c68 Merge remote-tracking branch 'origin/main' 2025-04-13 04:45:23 +08:00
耗子 cf6deb665c feat: format files 2025-04-13 04:45:18 +08:00
耗子 096890bd2f New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot
2025-04-13 04:42:27 +08:00
耗子 4dba4e5216 New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot
2025-04-13 04:42:25 +08:00
devhaozi 2b63c50434 chore(l10n): update frontend pot files 2025-04-12 20:22:39 +00:00
耗子 0a2d9262c9 feat: 统一翻译 2025-04-13 04:22:05 +08:00
耗子 4693cc8fe3 New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot
2025-04-13 03:41:34 +08:00
耗子 b6bad9ea64 New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot
2025-04-13 03:41:33 +08:00
耗子 a3b0bf10e9 Merge remote-tracking branch 'origin/main' 2025-04-13 03:41:07 +08:00
耗子 a627816694 feat: 优化样式 2025-04-13 03:40:56 +08:00
devhaozi a4ea9298a4 chore(l10n): update frontend pot files 2025-04-12 19:38:36 +00:00
耗子 233bb6e23b Merge remote-tracking branch 'origin/main' 2025-04-13 03:38:03 +08:00
耗子 bdd20107c7 feat: 调整部分翻译 2025-04-13 03:37:57 +08:00
devhaozi e296008112 chore(l10n): update frontend pot files 2025-04-12 19:37:47 +00:00
耗子 3dd01e6c1f Merge remote-tracking branch 'origin/main' 2025-04-13 03:37:10 +08:00
耗子 5b633e6528 feat: 调整部分翻译 2025-04-13 03:37:03 +08:00
devhaozi 2da260bba0 chore(l10n): update frontend pot files 2025-04-12 19:23:38 +00:00
耗子 acd8403e4b Merge remote-tracking branch 'origin/main' 2025-04-13 03:23:09 +08:00
耗子 90e66175ef feat: 调整部分翻译 2025-04-13 03:23:01 +08:00
devhaozi 4f1a7cc1bf chore(l10n): update frontend pot files 2025-04-12 19:16:38 +00:00
耗子 053d7d3ca5 Merge remote-tracking branch 'origin/main' 2025-04-13 03:16:08 +08:00
耗子 dae0e8274a feat: format files 2025-04-13 03:16:04 +08:00
devhaozi 2b3d1cd220 chore(l10n): update frontend pot files 2025-04-12 19:06:53 +00:00
耗子 5f0a4ca89e feat: format files 2025-04-13 03:06:22 +08:00
耗子 67d350ebe2 ci: 测试换一种清理方式 2025-04-13 02:44:06 +08:00
耗子 66c30f3e33 ci: 清理傻逼node_modules 2025-04-13 02:43:04 +08:00
耗子 82d763fdee Merge remote-tracking branch 'origin/main' 2025-04-13 02:42:45 +08:00
耗子 7159c4cccf ci: 清理傻逼node_modules 2025-04-13 02:42:40 +08:00
耗子 60357222df New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot
2025-04-13 02:40:14 +08:00
耗子 f6c43cf5a0 New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot
2025-04-13 02:40:13 +08:00
devhaozi f1341c5364 chore(l10n): update frontend pot files 2025-04-12 18:37:37 +00:00
耗子 b8cb8d70d3 ci: fix vue3-gettext 2025-04-13 02:37:03 +08:00
耗子 fa7fca1f11 feat: 完善翻译工作流 2025-04-13 02:35:22 +08:00
耗子 59cfa096b4 feat: 添加前端翻译 2025-04-13 02:26:23 +08:00
耗子 706c04a21f New translations frontend.pot (Chinese Traditional)
Update translations (Chinese Traditional) frontend.pot
2025-04-13 02:05:10 +08:00
耗子 255284caad New translations frontend.pot (Chinese Simplified)
Update translations (Chinese Simplified) frontend.pot
2025-04-13 02:05:09 +08:00
耗子 85b34fc017 New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot
2025-04-13 02:05:07 +08:00
耗子 d42deb54eb New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot
2025-04-13 02:05:06 +08:00
耗子 002104b0eb feat: 添加前端翻译 2025-04-13 01:55:19 +08:00
耗子 4dc943438e feat: 添加前端翻译 2025-04-13 01:45:40 +08:00
耗子 81a320cd93 feat: 添加前端翻译 2025-04-13 01:30:26 +08:00
耗子 df32b3b5de feat: 提交部分前端翻译 2025-04-13 01:14:11 +08:00
耗子 db0679cd92 feat: 提交部分前端翻译 2025-04-13 00:34:03 +08:00
耗子 20bbbc6202 Merge remote-tracking branch 'origin/main' 2025-04-13 00:28:02 +08:00
耗子 ce652f45e6 feat: 提交部分前端翻译 2025-04-13 00:27:56 +08:00
devhaozi c0f7183bc8 chore(l10n): update pot files 2025-04-12 15:24:19 +00:00
耗子 60d8ccd1a0 feat: 补全命令行翻译 2025-04-12 23:23:47 +08:00
耗子 e9561af93a Merge remote-tracking branch 'origin/main' 2025-04-12 23:01:30 +08:00
耗子 7e0ad2c799 feat: 前端支持翻译 2025-04-12 23:01:09 +08:00
renovate[bot] fe9f1c6e95 chore(deps): Update dependency @iconify/json to v2.2.326 (#663)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-12 22:16:29 +08:00
耗子 193ec9a6ae l10n: update translations (#666)
* New translations backend.pot (Chinese Simplified)
Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
Update translations (Chinese Traditional) backend.pot
2025-04-12 22:16:14 +08:00
耗子 72c3bed203 feat: 初始化前端翻译 2025-04-12 22:10:34 +08:00
耗子 2ae259e53a Merge remote-tracking branch 'origin/main' 2025-04-12 20:40:55 +08:00
耗子 3cd25e19de feat: 更新翻译不跳过ci 2025-04-12 20:40:49 +08:00
耗子 9bdffedb7e l10n: update translations (#664)
* New translations backend.pot (Chinese Simplified)
[skip ci] Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
[skip ci] Update translations (Chinese Traditional) backend.pot
2025-04-12 20:37:19 +08:00
耗子 c298a7182e feat: 不限制locale 2025-04-12 20:33:14 +08:00
耗子 d613a06ad2 Merge remote-tracking branch 'origin/main' 2025-04-12 20:15:55 +08:00
耗子 cc2cdaa76b fix: test 2025-04-12 20:15:50 +08:00
devhaozi 2c38a51515 chore(l10n): update pot files 2025-04-12 11:58:49 +00:00
耗子 d2e89353c5 Merge remote-tracking branch 'origin/main' 2025-04-12 19:58:24 +08:00
耗子 998bc02f8c fix: 翻译错误 2025-04-12 19:58:19 +08:00
devhaozi 42e3e20eb8 chore(l10n): update pot files 2025-04-12 11:55:24 +00:00
耗子 225795db74 fix: 翻译错误 2025-04-12 19:54:58 +08:00
耗子 bd35c42eb0 Merge remote-tracking branch 'origin/main' 2025-04-12 19:53:37 +08:00
耗子 d2587e77d4 fix: 翻译错误 2025-04-12 19:53:33 +08:00
devhaozi b0018ee71b chore(l10n): update pot files 2025-04-12 11:50:23 +00:00
耗子 985f4a6e6b fix: 翻译错误 2025-04-12 19:49:50 +08:00
devhaozi cf910c111f chore(l10n): update pot files 2025-04-12 11:23:51 +00:00
耗子 d7e0cb02cd fix: 翻译错误 2025-04-12 19:23:25 +08:00
耗子 c9c6c8b23a Merge remote-tracking branch 'origin/main' 2025-04-12 19:20:06 +08:00
耗子 3d0cb594ae fix: wire gen 2025-04-12 19:20:01 +08:00
devhaozi b614c9097b chore(l10n): update pot files 2025-04-12 11:18:20 +00:00
耗子 78b94fe89c fix: add translation 2025-04-12 19:17:52 +08:00
耗子 90140f4229 fix: lint 2025-04-12 19:09:55 +08:00
devhaozi d36c3b0dae chore(l10n): update pot files 2025-04-12 11:02:14 +00:00
耗子 88b64d6fc8 Merge remote-tracking branch 'origin/main' 2025-04-12 19:01:43 +08:00
耗子 ce4725ed09 fix: lint 2025-04-12 19:01:38 +08:00
devhaozi a2efa9a70b chore(l10n): update pot files 2025-04-12 10:58:38 +00:00
耗子 5126e2030a fix: lint 2025-04-12 18:58:08 +08:00
devhaozi b7b5744f17 chore(l10n): update pot files 2025-04-12 10:56:15 +00:00
耗子 0f08c10f02 feat: 添加翻译 2025-04-12 18:55:47 +08:00
devhaozi 6b4154f4a6 chore(l10n): update pot files 2025-04-12 10:41:36 +00:00
耗子 2d45e84b66 feat: 添加翻译 2025-04-12 18:41:08 +08:00
devhaozi 3f90402164 chore(l10n): update pot files 2025-04-12 09:35:50 +00:00
耗子 53fdcfb715 feat: 添加翻译 2025-04-12 17:35:22 +08:00
耗子 2ff01579d6 l10n: update translations (#660)
* New translations cli.pot (Chinese Simplified)
[skip ci] Update translations (Chinese Simplified) cli.pot

* New translations cli.pot (Chinese Traditional)
[skip ci] Update translations (Chinese Traditional) cli.pot

* New translations cli.pot (English)
[skip ci] Update translations (English) cli.pot

* New translations web.pot (Chinese Simplified)
[skip ci] Update translations (Chinese Simplified) web.pot

* New translations web.pot (Chinese Traditional)
[skip ci] Update translations (Chinese Traditional) web.pot

* New translations web.pot (English)
[skip ci] Update translations (English) web.pot

* New translations backend.pot (Chinese Simplified)
[skip ci] Update translations (Chinese Simplified) backend.pot

* New translations backend.pot (Chinese Traditional)
[skip ci] Update translations (Chinese Traditional) backend.pot

* New translations backend.pot (English)
[skip ci] Update translations (English) backend.pot
2025-04-12 06:40:16 +08:00
renovate[bot] 774d13980c chore(deps): Update dependency @types/node to v22.14.1 (#661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-12 06:39:47 +08:00
耗子 ae76a0171d fix: lint 2025-04-12 06:13:56 +08:00
耗子 137665b77d fix: lint 2025-04-12 05:57:26 +08:00
耗子 3b7bf35bf3 fix: lint 2025-04-12 05:55:18 +08:00
耗子 0b0c4da3eb fix: crowdin配置目录错误 2025-04-12 05:39:15 +08:00
耗子 7dfcfb98d5 feat: 添加crowdin配置 2025-04-12 05:33:34 +08:00
devhaozi 00b9be6bc9 chore(l10n): update pot files 2025-04-11 21:26:07 +00:00
耗子 771a9d1f28 feat: 完成应用翻译 2025-04-12 05:25:25 +08:00
耗子 3021f466f0 feat: 初步支持l10n 2025-04-12 04:27:10 +08:00
耗子 486a9fb58b feat: 移动embed到pkg下 2025-04-12 03:19:55 +08:00
耗子 dbc3050f2b feat: 更新php拓展,close #658 2025-04-12 02:15:47 +08:00
耗子 dc584a9382 fix: 网站默认使用lets证书,close #659 2025-04-12 00:30:59 +08:00
renovate[bot] e1f6afe08f chore(deps): Update dependency vite to v6.2.6 (#656)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-10 12:34:11 +00:00
renovate[bot] a7c95d929f chore(deps): Update non-major dependencies (#655)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-09 23:29:20 +08:00
耗子 9ff5596aca feat: 临时屏蔽libdns更新 2025-04-09 23:03:32 +08:00
耗子 4369eedfe5 feat: 临时屏蔽libdns更新 2025-04-09 22:57:28 +08:00
renovate[bot] 5af46a5039 chore(deps): Lock file maintenance (#654)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-07 07:20:36 +00:00
renovate[bot] 4e854515c8 chore(deps): Update module golang.org/x/crypto to v0.37.0 (#653)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-06 22:56:10 +00:00
renovate[bot] 552487b9d3 chore(deps): Update non-major dependencies (#652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-05 22:48:16 +00:00
耗子 8cadfe3e53 Merge remote-tracking branch 'origin/main' 2025-04-05 20:18:50 +08:00
耗子 15c0f28252 fix: lint 2025-04-05 20:18:45 +08:00
renovate[bot] 8e4b73c695 chore(deps): Update non-major dependencies (#651)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-05 20:11:09 +08:00
耗子 cb8f2a0ee1 Merge remote-tracking branch 'origin/main' 2025-04-05 20:10:59 +08:00
耗子 e6b3858937 fix: lint 2025-04-05 20:10:53 +08:00
renovate[bot] 1bdcf0473e chore(deps): Update golangci/golangci-lint-action action to v7 (#644)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-05 19:57:18 +08:00
耗子 edb5e858e4 fix: lint 2025-04-05 19:56:57 +08:00
renovate[bot] cfedad8a1b chore(deps): Update non-major dependencies (#650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-05 06:30:17 +00:00
renovate[bot] 08a5c66e62 chore(deps): Update non-major dependencies (#649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-04 06:38:29 +00:00
renovate[bot] 1d59fad7ad chore(deps): Update non-major dependencies (#648)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-03 13:46:17 +00:00
renovate[bot] e0145ee9e6 chore(deps): Update non-major dependencies (#647)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-03 05:14:09 +00:00
renovate[bot] 4703d8c3ca chore(deps): Update non-major dependencies (#646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 14:31:13 +00:00
renovate[bot] 8b95095e55 chore(deps): Update non-major dependencies (#645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-01 23:00:25 +00:00
耗子 709c345201 fix: 允许修改上传的证书 2025-04-01 17:18:30 +08:00
renovate[bot] 288968006a chore(deps): Update non-major dependencies (#643)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 16:12:01 +00:00
renovate[bot] 7f619f786d chore(deps): Lock file maintenance (#642)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 06:04:10 +00:00
耗子 c9c3de1348 feat: 添加反代说明 2025-03-30 20:02:21 +08:00
耗子 841b1d7d1b feat: 反代配置生成器 2025-03-30 17:51:41 +08:00
耗子 6f9d5370e0 feat: 移除阿里dns的日志输出 2025-03-30 15:41:52 +08:00
耗子 c8f12b3a89 feat: 优化dns处理 2025-03-30 15:27:53 +08:00
耗子 daa3407505 fix: dns 2025-03-30 05:44:43 +08:00
耗子 f5d2d2204f fix: 新建证书后dns账号错误设置0 2025-03-30 04:30:33 +08:00
耗子 04ef3ce442 feat: 更准确的证书续签判断 2025-03-30 04:19:30 +08:00
耗子 aff68bd1c1 chore: 只创建zip安装包 2025-03-30 03:49:55 +08:00
耗子 fca7065dca feat(证书): 支持西部数码 2025-03-30 03:40:08 +08:00
耗子 29330ec457 feat: 新增一批dns提供商 2025-03-30 02:08:40 +08:00
耗子 cc15852944 feat: 新增一批dns提供商 2025-03-30 01:48:07 +08:00
耗子 ecc44b2e92 fix: fuse文件系统没有inode 2025-03-29 17:03:05 +08:00
耗子 2b1e21ba4b fix: test 2025-03-29 15:04:00 +08:00
耗子 7a22469fb1 chore: update deps 2025-03-29 15:02:15 +08:00
耗子 31c063ba68 feat: acme日志记录 2025-03-29 14:57:35 +08:00
耗子 dad868b64c fix: 重写迁移 2025-03-29 14:44:56 +08:00
耗子 16de31c838 Merge remote-tracking branch 'origin/main' 2025-03-29 14:38:28 +08:00
耗子 3c1062a006 fix: 严格所有字段的默认值 2025-03-29 14:38:22 +08:00
renovate[bot] 7017aff841 chore(deps): Update module github.com/go-gormigrate/gormigrate/v2 to v2.1.4 (#640)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-28 23:01:02 +00:00
耗子 334c4bc4c1 chore: update renovate.json 2025-03-29 03:31:51 +08:00
耗子 e07a69ab67 fix: lint 2025-03-28 22:39:35 +08:00
renovate[bot] f4e31c4902 chore(deps): Lock file maintenance (#638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-28 22:27:14 +08:00
renovate[bot] b2d382c2af chore(deps): Update module github.com/tnb-labs/acmez/v3 to v3.0.0-20250328123917-f204dcee9d09 (#639)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-28 22:25:48 +08:00
耗子 5d0ad333c9 Merge branch 'main' of https://github.com/tnb-labs/panel 2025-03-28 22:24:13 +08:00
耗子 ccf05b8c42 feat: 新增minio 2025-03-28 22:23:05 +08:00
renovate[bot] 684c1b7140 chore(deps): Update module golang.org/x/net to v0.38.0 (#637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-28 01:52:42 +00:00
renovate[bot] 15e12d083d chore(deps): Update non-major dependencies (#636)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-27 15:10:51 +08:00
renovate[bot] f6937bb4cb chore(deps): Update dependency cronstrue to v2.57.0 (#635)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-27 04:01:53 +08:00
耗子 8683a85640 feat: 备份支持上传 2025-03-27 03:59:47 +08:00
耗子 7ee6dd4d0e Merge remote-tracking branch 'origin/main' 2025-03-27 03:20:49 +08:00
耗子 1c7c6074a1 feat: 优化备份与恢复体验 2025-03-27 03:20:40 +08:00
devhaozi af865b62a5 chore: update mocks 2025-03-26 18:56:30 +00:00
耗子 69b430d7cb feat(#108): 容器编排 2025-03-27 02:55:45 +08:00
devhaozi 0956385284 chore: update mocks 2025-03-26 18:23:27 +00:00
耗子 46cf1fa7da feat: 优化编排请求体 2025-03-27 02:22:43 +08:00
renovate[bot] ac9b42ca4d chore(deps): Update pnpm to v10.7.0 (#634)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 23:56:06 +08:00
renovate[bot] 264335429a chore(deps): Update dependency unplugin-auto-import to v19.1.2 (#633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 23:52:51 +08:00
renovate[bot] 323e2bf95a chore(deps): Update non-major dependencies (#632)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-25 23:12:45 +00:00
renovate[bot] 6b413d2962 chore(deps): Update module github.com/mholt/acmez/v3 to v3.1.1 (#631)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-25 06:43:44 +00:00
renovate[bot] 83e0e33207 chore(deps): Update non-major dependencies (#630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 22:08:51 +00:00
renovate[bot] 827df91241 chore(deps): Lock file maintenance (#629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 06:32:21 +00:00
renovate[bot] f78f414c77 chore(deps): Update dependency md-editor-v3 to v5.4.4 (#628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 14:55:33 +08:00
renovate[bot] dfb73d1730 chore(deps): Update non-major dependencies (#627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-22 01:22:59 +00:00
renovate[bot] 92c1d136c2 chore(deps): Update non-major dependencies (#626)
* chore(deps): Update non-major dependencies

* chore: update mocks

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-21 16:05:14 +00:00
耗子 45428f3e0b feat: 支持更新编排文件 2025-03-21 03:48:51 +08:00
耗子 139679ed0f feat: 支持更新编排文件 2025-03-21 03:21:44 +08:00
耗子 eed834bd5f feat: 支持更新编排文件 2025-03-21 03:20:08 +08:00
耗子 422c92cca5 feat: 创建compose支持env 2025-03-21 03:18:35 +08:00
耗子 28d699c886 fix: compose中的错误 2025-03-21 02:14:48 +08:00
耗子 f4533c9662 feat: 网站日志备份后重载Nginx 2025-03-20 19:23:41 +08:00
devhaozi ad71a8075b chore: update mocks 2025-03-19 18:52:30 +00:00
耗子 7d39015d97 feat: compose接口 2025-03-20 02:49:18 +08:00
renovate[bot] c3498aa5c5 chore(deps): Update pnpm to v10.6.5 (#624)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-19 16:39:33 +00:00
renovate[bot] 2c3bb5bf91 chore(deps): Update non-major dependencies (#623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-18 10:19:09 +00:00
renovate[bot] 3c2b6e6488 chore(deps): Update dependency sass to v1.86.0 (#622)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 23:10:52 +00:00
耗子 dff0a61182 feat(#528): 证书支持部署脚本 2025-03-18 03:18:39 +08:00
耗子 d081f1e312 feat(#570): 为部分应用添加运行日志 2025-03-18 02:55:28 +08:00
耗子 46809c8d5b fix: build 2025-03-18 02:45:42 +08:00
耗子 f1246ee779 fix(#612): 优化复制功能 2025-03-18 02:41:06 +08:00
renovate[bot] 03168a5652 chore(deps): Update dependency @vitejs/plugin-vue to v5.2.3 (#621)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 18:31:06 +00:00
renovate[bot] 0e43d25430 chore(deps): Update dependency md-editor-v3 to v5.4.2 (#620)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 11:14:40 +00:00
renovate[bot] 46a9446007 chore(deps): Lock file maintenance (#619)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 03:43:57 +00:00
renovate[bot] 3761223230 chore(deps): Update dependency @vitejs/plugin-vue to v5.2.2 (#618)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 03:43:36 +00:00
renovate[bot] 836b34e59d chore(deps): Update module github.com/expr-lang/expr to v1.17.0 (#617)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-16 10:20:11 +00:00
renovate[bot] 3dc1106012 chore(deps): Update dependency @guolao/vue-monaco-editor to v1.5.5 (#616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-15 10:56:09 +00:00
renovate[bot] 3eb8bf793f chore(deps): Update dependency @iconify/json to v2.2.317 (#615)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 19:37:18 +00:00
renovate[bot] cd823c4506 chore(deps): Update module github.com/go-rat/utils to v1.1.4 (#614)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 12:05:54 +00:00
renovate[bot] 36d45ee66f chore(deps): Update non-major dependencies (#611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 06:33:53 +00:00
renovate[bot] 068d36de3d chore(deps): Update module github.com/ncruces/go-sqlite3 to v0.24.1 (#609)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-14 01:44:22 +00:00
renovate[bot] e3607b9bc7 chore(deps): Update non-major dependencies (#608)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-12 22:43:43 +00:00
耗子 76a2ff8e43 Merge remote-tracking branch 'origin/main' 2025-03-12 23:08:57 +08:00
耗子 b3e3da6833 feat: 优化关闭数据库连接 2025-03-12 23:08:46 +08:00
dependabot[bot] fa6c574743 chore(deps): bump @babel/helpers (#607)
Bumps the npm_and_yarn group with 1 update in the /web directory: [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers).


Updates `@babel/helpers` from 7.26.9 to 7.26.10
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 22:01:00 +08:00
耗子 e7625a77a8 feat: 优化SQLite配置防止数据库损坏 2025-03-12 21:58:06 +08:00
耗子 e1b3c80771 Merge branch 'main' of https://github.com/tnb-labs/panel 2025-03-12 21:57:53 +08:00
耗子 3e9228cb65 feat: 优化SQLite配置防止数据库损坏 2025-03-12 21:57:06 +08:00
renovate[bot] e8b5bb266f chore(deps): Update non-major dependencies (#606)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-12 11:04:01 +00:00
耗子 a6d27f261b fix: 容器主机端口显示错误 2025-03-12 03:05:07 +08:00
renovate[bot] 1a85571fca chore(deps): Update dependency @rushstack/eslint-patch to v1.11.0 (#605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-11 02:38:42 +00:00
renovate[bot] 9b4f308367 chore(deps): Update module github.com/go-rat/utils to v1.1.0 (#604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 22:10:38 +00:00
renovate[bot] aabf49d041 chore(deps): Update module github.com/tnb-labs/acmez/v3 to v3.0.0-20250310074100-3f4cd473d1db (#603)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 09:56:11 +00:00
renovate[bot] 0d22b53b36 chore(deps): Update dependency @vueuse/core to v13 (#601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 15:46:39 +08:00
renovate[bot] 8d9c25c195 chore(deps): Update dependency @iconify/json to v2.2.315 (#602)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 07:42:28 +00:00
耗子 bfd4fe41d3 ci: fix mockery 2025-03-10 15:38:17 +08:00
renovate[bot] 8400705053 chore(deps): Lock file maintenance (#600)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 03:11:47 +00:00
renovate[bot] b3e72acad8 chore(deps): Update dependency @types/node to v22.13.10 (#599)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-08 10:42:33 +00:00
renovate[bot] 33884c4c67 chore(deps): Update dependency eslint to v9.22.0 (#598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 22:35:54 +00:00
renovate[bot] 45a939571d chore(deps): Update dependency alova to v3.2.10 (#597)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 14:24:15 +00:00
renovate[bot] ac4f2a8531 chore(deps): Update non-major dependencies (#596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 07:07:55 +00:00
renovate[bot] 7c09b017c9 chore(deps): Update non-major dependencies (#595)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-06 11:05:41 +00:00
renovate[bot] 02c207d0b8 chore(deps): Update module golang.org/x/net to v0.37.0 (#594)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 22:08:03 +00:00
renovate[bot] 6d23f59d6a chore(deps): Update module golang.org/x/crypto to v0.36.0 (#593)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 19:17:35 +00:00
耗子 4c55552dec fix(#592): 某些情况下计划任务创建失败 2025-03-06 01:21:48 +08:00
renovate[bot] 5ba24d11f6 chore(deps): Update dependency @vueuse/core to v12.8.2 (#591)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 11:56:15 +00:00
renovate[bot] bcf557c47e chore(deps): Update dependency eslint-plugin-vue to v10 (#590)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 15:57:39 +08:00
renovate[bot] cac55a4f79 chore(deps): Update dependency @vueuse/core to v12.8.0 (#589)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 06:58:44 +00:00
renovate[bot] 0d26edf6f0 chore(deps): Update dependency eslint-plugin-vue to v9.33.0 (#588)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 03:09:56 +00:00
renovate[bot] 7a39ef706c chore(deps): Update module golang.org/x/net to v0.36.0 (#587)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 23:17:00 +00:00
renovate[bot] f5c37c0402 chore(deps): Update dependency @iconify/json to v2.2.313 (#586)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 12:03:56 +00:00
renovate[bot] 16d571e4ea chore(deps): Update module github.com/go-rat/chix to v1.1.5 (#585)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 22:44:10 +00:00
renovate[bot] b4b75aa9c5 chore(deps): Update dependency @types/node to v22.13.9 (#584)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 19:33:49 +00:00
renovate[bot] 6ed7784a89 chore(deps): Update dependency cronstrue to v2.56.0 (#583)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 15:34:59 +00:00
renovate[bot] d2c7c1109d chore(deps): Lock file maintenance (#582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 02:58:02 +00:00
renovate[bot] 079a7f7c07 chore(deps): Update dependency prettier to v3.5.3 (#581)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 02:57:19 +00:00
renovate[bot] 923c6a478c chore(deps): Update dependency vue-tsc to v2.2.8 (#580)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-02 12:45:26 +00:00
renovate[bot] 9e0f65cf52 chore(deps): Lock file maintenance (#579)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-02 12:45:07 +00:00
renovate[bot] eab8184b6b chore(deps): Update non-major dependencies (#578)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-02 09:42:37 +00:00
renovate[bot] 3972a17bd1 chore(deps): Update non-major dependencies (#577)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 10:27:29 +00:00
renovate[bot] 34b7b8a10a chore(deps): Update dependency @types/node to v22.13.7 (#576)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-01 01:42:19 +00:00
renovate[bot] a4da49d8ae chore(deps): Update dependency @types/node to v22.13.6 (#575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-28 21:52:59 +00:00
renovate[bot] f8a989fbfc chore(deps): Update non-major dependencies (#574)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-28 19:37:18 +00:00
renovate[bot] 4497b8fa82 chore(deps): Update dependency vite-plugin-static-copy to v2.3.0 (#573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-28 14:52:48 +00:00
耗子 0bb7ccd121 Merge remote-tracking branch 'origin/main' 2025-02-28 16:06:20 +08:00
耗子 b90227017c fix: ssh连接成功后即立即调整窗口大小 2025-02-28 16:04:20 +08:00
renovate[bot] 75d941060f chore(deps): Update dependency alova to v3.2.9 (#572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-28 06:17:59 +00:00
renovate[bot] b37be8aae9 chore(deps): Update dependency @iconify/json to v2.2.311 (#571)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 07:17:35 +00:00
renovate[bot] f5a471768b chore(deps): Update non-major dependencies (#569)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 03:43:49 +00:00
renovate[bot] f6ad6b23cb chore(deps): Update github.com/tufanbarisyildirim/gonginx digest to c03497d (#568)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 22:47:05 +00:00
renovate[bot] f5b9d3e912 chore(deps): Update dependency vite to v6.2.0 (#567)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 05:51:07 +00:00
renovate[bot] 9619c3f55b chore(deps): Update dependency sass to v1.85.1 (#566)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 02:38:56 +00:00
renovate[bot] 8c08ebb041 chore(deps): Update module golang.org/x/crypto to v0.35.0 (#565)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 18:52:49 +00:00
renovate[bot] 0567319671 chore(deps): Update module github.com/ncruces/go-sqlite3 to v0.23.3 (#564)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 15:34:06 +00:00
renovate[bot] 760c3be9c7 chore(deps): Update dependency @iconify/json to v2.2.310 (#563)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 09:25:24 +00:00
耗子 1084968e99 chore: update ubuntu 2025-02-24 17:21:04 +08:00
renovate[bot] 188c2ffce0 chore(deps): Lock file maintenance (#562)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 03:10:59 +00:00
renovate[bot] f6296e7704 chore(deps): Update dependency vue-tsc to v2.2.4 (#561)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-23 14:38:04 +00:00
renovate[bot] bd149f6524 chore(deps): Update dependency prettier to v3.5.2 (#560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-22 06:51:11 +00:00
renovate[bot] 989beb3642 chore(deps): Update module golang.org/x/crypto to v0.34.0 (#559)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-22 01:33:27 +00:00
renovate[bot] 5d4d94a5d8 chore(deps): Update non-major dependencies (#558)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 22:06:00 +00:00
renovate[bot] 41eca93f12 chore(deps): Update dependency md-editor-v3 to v5.3.2 (#557)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 11:18:40 +00:00
renovate[bot] 946def76c5 chore(deps): Lock file maintenance (#556)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 16:09:27 +00:00
renovate[bot] 24768a4078 chore(deps): Update non-major dependencies (#555)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 16:08:52 +00:00
renovate[bot] 7751117dc6 chore(deps): Update dependency md-editor-v3 to v5.3.0 (#554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 09:56:51 +00:00
renovate[bot] f9f286335d chore(deps): Update unocss monorepo to v66 (#550)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 15:47:23 +08:00
renovate[bot] 1d50da79bb chore(deps): Update dependency vite to v6.1.1 (#553)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-19 12:51:55 +00:00
renovate[bot] 7e78b6eaa3 chore(deps): Update dependency unplugin-vue-components to v28.4.0 (#552)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-19 06:18:58 +00:00
renovate[bot] a098c988a7 chore(deps): Update dependency tsx to v4.19.3 (#551)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-19 01:36:51 +00:00
renovate[bot] 0b04f32aed chore(deps): Update non-major dependencies (#549)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 12:18:21 +00:00
renovate[bot] e14c1db060 chore(deps): Update non-major dependencies (#548)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 06:41:47 +00:00
renovate[bot] e109e16610 chore(deps): Update dependency md-editor-v3 to v5.2.3 (#547)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 05:47:45 +00:00
renovate[bot] cd3f2b5951 chore(deps): Lock file maintenance (#546)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 02:39:56 +00:00
耗子 fe28e84bf7 Merge remote-tracking branch 'origin/main' 2025-02-16 16:08:51 +08:00
耗子 fe4e3850c9 feat: test new sqlite driver 2025-02-16 16:07:22 +08:00
renovate[bot] 2a070e14c0 chore(deps): Update dependency vue-tsc to v2.2.2 (#545)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-15 18:32:15 +00:00
renovate[bot] 3984f85d57 chore(deps): Update non-major dependencies (#544)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-15 10:36:05 +00:00
耗子 dcd4eb1e49 Merge remote-tracking branch 'origin/main' 2025-02-15 02:48:35 +08:00
耗子 bc62fbadbb fix: redis info 2025-02-15 02:47:50 +08:00
renovate[bot] d17ef0bfda chore(deps): Update non-major dependencies (#543)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 18:07:07 +00:00
renovate[bot] 622d08f6f4 chore(deps): Update dependency @vueuse/core to v12.6.1 (#542)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 06:44:38 +00:00
renovate[bot] 2a2fbbf287 chore(deps): Update dependency sass to v1.85.0 (#541)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-14 03:27:12 +00:00
renovate[bot] 77fa6ec58f chore(deps): Update dependency @types/node to v22.13.4 (#540)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-13 23:44:36 +00:00
renovate[bot] fae4894cea chore(deps): Update dependency prettier to v3.5.1 (#539)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-13 16:12:53 +00:00
耗子 b4195459c5 Merge remote-tracking branch 'origin/main' 2025-02-13 17:22:38 +08:00
耗子 5a40cd3910 fix: 优化备份与清理命令输出 2025-02-13 17:21:37 +08:00
renovate[bot] c6971004f5 chore(deps): Update non-major dependencies (#538)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-13 06:27:58 +00:00
renovate[bot] e6ff46ccac chore(deps): Update dependency pinia to v3.0.1 (#537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-12 18:36:52 +00:00
357 changed files with 39569 additions and 9121 deletions
+4 -4
View File
@@ -6,14 +6,13 @@ on:
pull_request:
jobs:
frontend:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
run_install: true
package_json_file: web/package.json
- name: Setup Node.js
@@ -27,6 +26,7 @@ jobs:
run: |
cp .env.production .env
cp settings/proxy-config.example.ts settings/proxy-config.ts
pnpm run gettext:compile
pnpm build
- name: Upload frontend
uses: actions/upload-artifact@v4
@@ -35,7 +35,7 @@ jobs:
path: web/dist/
backend:
needs: frontend
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
strategy:
matrix:
goarch: [ amd64, arm64 ]
@@ -56,7 +56,7 @@ jobs:
uses: actions/download-artifact@v4
with:
name: frontend
path: internal/embed/frontend
path: pkg/embed/frontend
- name: Set build info
run: |
echo "VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo '0.0.0')" >> $GITHUB_ENV
+2 -4
View File
@@ -7,7 +7,7 @@ permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -21,7 +21,6 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
run_install: true
package_json_file: web/package.json
- name: Setup Node.js
@@ -35,13 +34,12 @@ jobs:
run: |
cp .env.production .env
cp settings/proxy-config.example.ts settings/proxy-config.ts
pnpm run gettext:compile
pnpm build
- name: Set environment variables
run: |
echo "GOVERSION=$(go version | cut -d' ' -f3)" >> $GITHUB_ENV
echo "HOSTNAME=$(hostname)" >> $GITHUB_ENV
- name: Clear node_modules
run: rm -rf node_modules
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: ☢️ Bug
if: github.event.label.name == '☢️ Bug'
+52
View File
@@ -0,0 +1,52 @@
name: L10n
on:
push:
branches:
- main
jobs:
xgotext:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
cache: true
go-version: 'stable'
- name: Install xgotext
run: |
go install github.com/leonelquinteros/gotext/cli/xgotext@latest
- name: Generate pot files
run: |
~/go/bin/xgotext -default backend -pkg-tree ./cmd/web -out ./pkg/embed/locales
git pull
- uses: stefanzweifel/git-auto-commit-action@v5
name: Commit changes
with:
commit_message: "chore(l10n): update backend pot files"
vue3-gettext:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
run_install: true
package_json_file: web/package.json
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache-dependency-path: web/pnpm-lock.yaml
- name: Generate pot files and merge po files
working-directory: web
run: |
pnpm run gettext:extract
git pull
- uses: stefanzweifel/git-auto-commit-action@v5
name: Commit changes
with:
commit_message: "chore(l10n): update frontend pot files"
+4 -5
View File
@@ -9,7 +9,7 @@ permissions:
jobs:
golangci:
name: golanci-lint
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -19,13 +19,13 @@ jobs:
cache: true
go-version: 'stable'
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v8
with:
skip-cache: true
version: latest
args: --timeout=30m ./...
govulncheck:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -39,14 +39,13 @@ jobs:
- name: Run Govulncheck
run: govulncheck ./...
frontend:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
run_install: true
package_json_file: web/package.json
- name: Setup Node.js
+6 -1
View File
@@ -3,19 +3,24 @@ on:
push:
branches:
- main
pull_request:
jobs:
mockery:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
cache: true
go-version: 'stable'
- name: Install Mockery
run: |
go install github.com/vektra/mockery/v2@latest
- name: Generate Mocks
run: |
~/go/bin/mockery
git pull
- uses: stefanzweifel/git-auto-commit-action@v5
name: Commit changes
with:
+1 -1
View File
@@ -6,7 +6,7 @@ on:
pull_request:
jobs:
unit:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
+3
View File
@@ -63,3 +63,6 @@ out/
*.sublime*
__debug_bin
.project
# 傻逼 node_modules
node_modules/
+1 -1
View File
@@ -60,7 +60,7 @@ archives:
builds:
- web
- cli
formats: ["zip", "tar.gz", "tar.xz"]
formats: ["zip"]
wrap_in_directory: false
strip_binary_directory: true
files:
+40 -36
View File
@@ -8,12 +8,13 @@ package main
import (
"github.com/tnb-labs/panel/internal/app"
"github.com/tnb-labs/panel/internal/apps/benchmark"
"github.com/tnb-labs/panel/internal/apps/codeserver"
"github.com/tnb-labs/panel/internal/apps/docker"
"github.com/tnb-labs/panel/internal/apps/fail2ban"
"github.com/tnb-labs/panel/internal/apps/frp"
"github.com/tnb-labs/panel/internal/apps/gitea"
"github.com/tnb-labs/panel/internal/apps/memcached"
"github.com/tnb-labs/panel/internal/apps/minio"
"github.com/tnb-labs/panel/internal/apps/mysql"
"github.com/tnb-labs/panel/internal/apps/nginx"
"github.com/tnb-labs/panel/internal/apps/php74"
@@ -30,7 +31,6 @@ import (
"github.com/tnb-labs/panel/internal/apps/rsync"
"github.com/tnb-labs/panel/internal/apps/s3fs"
"github.com/tnb-labs/panel/internal/apps/supervisor"
"github.com/tnb-labs/panel/internal/apps/toolbox"
"github.com/tnb-labs/panel/internal/bootstrap"
"github.com/tnb-labs/panel/internal/data"
"github.com/tnb-labs/panel/internal/route"
@@ -49,6 +49,10 @@ func initCli() (*app.Cli, error) {
if err != nil {
return nil, err
}
locale, err := bootstrap.NewT(koanf)
if err != nil {
return nil, err
}
logger := bootstrap.NewLog(koanf)
db, err := bootstrap.NewDB(koanf, logger)
if err != nil {
@@ -56,45 +60,45 @@ func initCli() (*app.Cli, error) {
}
cacheRepo := data.NewCacheRepo(db)
queue := bootstrap.NewQueue()
taskRepo := data.NewTaskRepo(db, logger, queue)
appRepo := data.NewAppRepo(db, cacheRepo, taskRepo)
userRepo := data.NewUserRepo(db)
settingRepo := data.NewSettingRepo(db, koanf, taskRepo)
databaseServerRepo := data.NewDatabaseServerRepo(db, logger)
databaseUserRepo := data.NewDatabaseUserRepo(db, databaseServerRepo)
databaseRepo := data.NewDatabaseRepo(db, databaseServerRepo, databaseUserRepo)
certRepo := data.NewCertRepo(db)
certAccountRepo := data.NewCertAccountRepo(db, userRepo)
websiteRepo := data.NewWebsiteRepo(db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo)
backupRepo := data.NewBackupRepo(db, settingRepo, websiteRepo)
cliService := service.NewCliService(koanf, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo)
cli := route.NewCli(cliService)
command := bootstrap.NewCli(cli)
taskRepo := data.NewTaskRepo(locale, db, logger, queue)
appRepo := data.NewAppRepo(locale, db, cacheRepo, taskRepo)
userRepo := data.NewUserRepo(locale, db)
settingRepo := data.NewSettingRepo(locale, db, koanf, taskRepo)
databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger)
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, databaseServerRepo)
databaseRepo := data.NewDatabaseRepo(locale, db, databaseServerRepo, databaseUserRepo)
certRepo := data.NewCertRepo(locale, db, logger)
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo)
backupRepo := data.NewBackupRepo(locale, db, settingRepo, websiteRepo)
cliService := service.NewCliService(locale, koanf, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo)
cli := route.NewCli(locale, cliService)
command := bootstrap.NewCli(locale, cli)
gormigrate := bootstrap.NewMigrate(db)
benchmarkApp := benchmark.NewApp()
codeserverApp := codeserver.NewApp()
dockerApp := docker.NewApp()
fail2banApp := fail2ban.NewApp(websiteRepo)
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
frpApp := frp.NewApp()
giteaApp := gitea.NewApp()
memcachedApp := memcached.NewApp()
mysqlApp := mysql.NewApp(settingRepo)
nginxApp := nginx.NewApp()
php74App := php74.NewApp(taskRepo)
php80App := php80.NewApp(taskRepo)
php81App := php81.NewApp(taskRepo)
php82App := php82.NewApp(taskRepo)
php83App := php83.NewApp(taskRepo)
php84App := php84.NewApp(taskRepo)
phpmyadminApp := phpmyadmin.NewApp()
memcachedApp := memcached.NewApp(locale)
minioApp := minio.NewApp()
mysqlApp := mysql.NewApp(locale, settingRepo)
nginxApp := nginx.NewApp(locale)
php74App := php74.NewApp(locale, taskRepo)
php80App := php80.NewApp(locale, taskRepo)
php81App := php81.NewApp(locale, taskRepo)
php82App := php82.NewApp(locale, taskRepo)
php83App := php83.NewApp(locale, taskRepo)
php84App := php84.NewApp(locale, taskRepo)
phpmyadminApp := phpmyadmin.NewApp(locale)
podmanApp := podman.NewApp()
postgresqlApp := postgresql.NewApp()
pureftpdApp := pureftpd.NewApp()
redisApp := redis.NewApp()
rsyncApp := rsync.NewApp()
s3fsApp := s3fs.NewApp(settingRepo)
supervisorApp := supervisor.NewApp()
toolboxApp := toolbox.NewApp()
loader := bootstrap.NewLoader(benchmarkApp, dockerApp, fail2banApp, frpApp, giteaApp, memcachedApp, mysqlApp, nginxApp, php74App, php80App, php81App, php82App, php83App, php84App, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp, toolboxApp)
postgresqlApp := postgresql.NewApp(locale)
pureftpdApp := pureftpd.NewApp(locale)
redisApp := redis.NewApp(locale)
rsyncApp := rsync.NewApp(locale)
s3fsApp := s3fs.NewApp(locale)
supervisorApp := supervisor.NewApp(locale)
loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, memcachedApp, minioApp, mysqlApp, nginxApp, php74App, php80App, php81App, php82App, php83App, php84App, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
appCli := app.NewCli(command, gormigrate, loader)
return appCli, nil
}
+57 -47
View File
@@ -8,12 +8,13 @@ package main
import (
"github.com/tnb-labs/panel/internal/app"
"github.com/tnb-labs/panel/internal/apps/benchmark"
"github.com/tnb-labs/panel/internal/apps/codeserver"
"github.com/tnb-labs/panel/internal/apps/docker"
"github.com/tnb-labs/panel/internal/apps/fail2ban"
"github.com/tnb-labs/panel/internal/apps/frp"
"github.com/tnb-labs/panel/internal/apps/gitea"
"github.com/tnb-labs/panel/internal/apps/memcached"
"github.com/tnb-labs/panel/internal/apps/minio"
"github.com/tnb-labs/panel/internal/apps/mysql"
"github.com/tnb-labs/panel/internal/apps/nginx"
"github.com/tnb-labs/panel/internal/apps/php74"
@@ -30,7 +31,6 @@ import (
"github.com/tnb-labs/panel/internal/apps/rsync"
"github.com/tnb-labs/panel/internal/apps/s3fs"
"github.com/tnb-labs/panel/internal/apps/supervisor"
"github.com/tnb-labs/panel/internal/apps/toolbox"
"github.com/tnb-labs/panel/internal/bootstrap"
"github.com/tnb-labs/panel/internal/data"
"github.com/tnb-labs/panel/internal/http/middleware"
@@ -51,6 +51,10 @@ func initWeb() (*app.Web, error) {
if err != nil {
return nil, err
}
locale, err := bootstrap.NewT(koanf)
if err != nil {
return nil, err
}
logger := bootstrap.NewLog(koanf)
db, err := bootstrap.NewDB(koanf, logger)
if err != nil {
@@ -62,80 +66,86 @@ func initWeb() (*app.Web, error) {
}
cacheRepo := data.NewCacheRepo(db)
queue := bootstrap.NewQueue()
taskRepo := data.NewTaskRepo(db, logger, queue)
appRepo := data.NewAppRepo(db, cacheRepo, taskRepo)
middlewares := middleware.NewMiddlewares(koanf, logger, manager, appRepo)
userRepo := data.NewUserRepo(db)
userService := service.NewUserService(koanf, manager, userRepo)
databaseServerRepo := data.NewDatabaseServerRepo(db, logger)
databaseUserRepo := data.NewDatabaseUserRepo(db, databaseServerRepo)
databaseRepo := data.NewDatabaseRepo(db, databaseServerRepo, databaseUserRepo)
certRepo := data.NewCertRepo(db)
certAccountRepo := data.NewCertAccountRepo(db, userRepo)
websiteRepo := data.NewWebsiteRepo(db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo)
settingRepo := data.NewSettingRepo(db, koanf, taskRepo)
cronRepo := data.NewCronRepo(db)
backupRepo := data.NewBackupRepo(db, settingRepo, websiteRepo)
dashboardService := service.NewDashboardService(koanf, taskRepo, websiteRepo, appRepo, settingRepo, cronRepo, backupRepo)
taskRepo := data.NewTaskRepo(locale, db, logger, queue)
appRepo := data.NewAppRepo(locale, db, cacheRepo, taskRepo)
userTokenRepo := data.NewUserTokenRepo(locale, db)
middlewares := middleware.NewMiddlewares(koanf, logger, manager, appRepo, userTokenRepo)
userRepo := data.NewUserRepo(locale, db)
userService := service.NewUserService(locale, koanf, manager, userRepo)
userTokenService := service.NewUserTokenService(locale, userTokenRepo)
databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger)
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, databaseServerRepo)
databaseRepo := data.NewDatabaseRepo(locale, db, databaseServerRepo, databaseUserRepo)
certRepo := data.NewCertRepo(locale, db, logger)
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo)
settingRepo := data.NewSettingRepo(locale, db, koanf, taskRepo)
cronRepo := data.NewCronRepo(locale, db)
backupRepo := data.NewBackupRepo(locale, db, settingRepo, websiteRepo)
dashboardService := service.NewDashboardService(locale, koanf, taskRepo, websiteRepo, appRepo, settingRepo, cronRepo, backupRepo)
taskService := service.NewTaskService(taskRepo)
websiteService := service.NewWebsiteService(websiteRepo, settingRepo)
databaseService := service.NewDatabaseService(databaseRepo)
databaseServerService := service.NewDatabaseServerService(databaseServerRepo)
databaseUserService := service.NewDatabaseUserService(databaseUserRepo)
backupService := service.NewBackupService(backupRepo)
certService := service.NewCertService(certRepo)
backupService := service.NewBackupService(locale, backupRepo)
certService := service.NewCertService(locale, certRepo)
certDNSRepo := data.NewCertDNSRepo(db)
certDNSService := service.NewCertDNSService(certDNSRepo)
certAccountService := service.NewCertAccountService(certAccountRepo)
appService := service.NewAppService(appRepo, cacheRepo, settingRepo)
appService := service.NewAppService(locale, appRepo, cacheRepo, settingRepo)
cronService := service.NewCronService(cronRepo)
processService := service.NewProcessService()
safeRepo := data.NewSafeRepo()
safeService := service.NewSafeService(safeRepo)
firewallService := service.NewFirewallService()
sshRepo := data.NewSSHRepo(db)
sshRepo := data.NewSSHRepo(locale, db)
sshService := service.NewSSHService(sshRepo)
containerRepo := data.NewContainerRepo()
containerService := service.NewContainerService(containerRepo)
containerComposeRepo := data.NewContainerComposeRepo()
containerComposeService := service.NewContainerComposeService(containerComposeRepo)
containerNetworkRepo := data.NewContainerNetworkRepo()
containerNetworkService := service.NewContainerNetworkService(containerNetworkRepo)
containerImageRepo := data.NewContainerImageRepo()
containerImageService := service.NewContainerImageService(containerImageRepo)
containerVolumeRepo := data.NewContainerVolumeRepo()
containerVolumeService := service.NewContainerVolumeService(containerVolumeRepo)
fileService := service.NewFileService(taskRepo)
fileService := service.NewFileService(locale, taskRepo)
monitorRepo := data.NewMonitorRepo(db, settingRepo)
monitorService := service.NewMonitorService(settingRepo, monitorRepo)
settingService := service.NewSettingService(settingRepo)
systemctlService := service.NewSystemctlService()
benchmarkApp := benchmark.NewApp()
systemctlService := service.NewSystemctlService(locale)
toolboxSystemService := service.NewToolboxSystemService(locale)
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)
codeserverApp := codeserver.NewApp()
dockerApp := docker.NewApp()
fail2banApp := fail2ban.NewApp(websiteRepo)
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
frpApp := frp.NewApp()
giteaApp := gitea.NewApp()
memcachedApp := memcached.NewApp()
mysqlApp := mysql.NewApp(settingRepo)
nginxApp := nginx.NewApp()
php74App := php74.NewApp(taskRepo)
php80App := php80.NewApp(taskRepo)
php81App := php81.NewApp(taskRepo)
php82App := php82.NewApp(taskRepo)
php83App := php83.NewApp(taskRepo)
php84App := php84.NewApp(taskRepo)
phpmyadminApp := phpmyadmin.NewApp()
memcachedApp := memcached.NewApp(locale)
minioApp := minio.NewApp()
mysqlApp := mysql.NewApp(locale, settingRepo)
nginxApp := nginx.NewApp(locale)
php74App := php74.NewApp(locale, taskRepo)
php80App := php80.NewApp(locale, taskRepo)
php81App := php81.NewApp(locale, taskRepo)
php82App := php82.NewApp(locale, taskRepo)
php83App := php83.NewApp(locale, taskRepo)
php84App := php84.NewApp(locale, taskRepo)
phpmyadminApp := phpmyadmin.NewApp(locale)
podmanApp := podman.NewApp()
postgresqlApp := postgresql.NewApp()
pureftpdApp := pureftpd.NewApp()
redisApp := redis.NewApp()
rsyncApp := rsync.NewApp()
s3fsApp := s3fs.NewApp(settingRepo)
supervisorApp := supervisor.NewApp()
toolboxApp := toolbox.NewApp()
loader := bootstrap.NewLoader(benchmarkApp, dockerApp, fail2banApp, frpApp, giteaApp, memcachedApp, mysqlApp, nginxApp, php74App, php80App, php81App, php82App, php83App, php84App, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp, toolboxApp)
http := route.NewHttp(userService, dashboardService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, cronService, processService, safeService, firewallService, sshService, containerService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, loader)
wsService := service.NewWsService(koanf, sshRepo)
postgresqlApp := postgresql.NewApp(locale)
pureftpdApp := pureftpd.NewApp(locale)
redisApp := redis.NewApp(locale)
rsyncApp := rsync.NewApp(locale)
s3fsApp := s3fs.NewApp(locale)
supervisorApp := supervisor.NewApp(locale)
loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, memcachedApp, minioApp, mysqlApp, nginxApp, php74App, php80App, php81App, php82App, php83App, php84App, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
http := route.NewHttp(userService, userTokenService, dashboardService, taskService, websiteService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, loader)
wsService := service.NewWsService(locale, koanf, sshRepo)
ws := route.NewWs(wsService)
mux, err := bootstrap.NewRouter(middlewares, http, ws)
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
if err != nil {
return nil, err
}
@@ -149,7 +159,7 @@ func initWeb() (*app.Web, error) {
if err != nil {
return nil, err
}
validation := bootstrap.NewValidator(db)
validation := bootstrap.NewValidator(koanf, db)
web := app.NewWeb(koanf, mux, server, gormigrate, cron, queue, validation)
return web, nil
}
+2
View File
@@ -11,3 +11,5 @@ http:
tls: true
database:
debug: false
session:
lifetime: 120
+7
View File
@@ -0,0 +1,7 @@
commit_message: 'Update translations (%language%) %original_file_name%'
pull_request_title: 'l10n: update translations'
files:
- source: /pkg/embed/locales/*.pot
translation: /pkg/embed/locales/%locale_with_underscore%/%file_name%.po
- source: /web/src/locales/*.pot
translation: /web/src/locales/%locale_with_underscore%.po
+61 -44
View File
@@ -1,92 +1,109 @@
module github.com/tnb-labs/panel
go 1.24
go 1.24.0
require (
github.com/bddjr/hlfhr v1.3.8
github.com/beevik/ntp v1.4.3
github.com/creack/pty v1.1.24
github.com/expr-lang/expr v1.16.9
github.com/glebarez/sqlite v1.11.0
github.com/devhaozi/westcn v0.0.0-20250407083704-b49375823872
github.com/expr-lang/expr v1.17.2
github.com/go-chi/chi/v5 v5.2.1
github.com/go-gormigrate/gormigrate/v2 v2.1.3
github.com/go-rat/chix v1.1.4
github.com/go-gormigrate/gormigrate/v2 v2.1.4
github.com/go-rat/chix v1.2.0
github.com/go-rat/gormstore v1.0.6
github.com/go-rat/sessions v1.0.14
github.com/go-rat/utils v1.0.9
github.com/go-rat/sessions v1.1.0
github.com/go-rat/utils v1.1.5
github.com/go-resty/resty/v2 v2.16.5
github.com/go-sql-driver/mysql v1.8.1
github.com/go-sql-driver/mysql v1.9.2
github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a
github.com/gomodule/redigo v1.9.2
github.com/google/wire v0.6.0
github.com/gookit/color v1.5.4
github.com/gookit/validate v1.5.4
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-version v1.7.0
github.com/knadh/koanf/parsers/yaml v0.1.0
github.com/knadh/koanf/providers/file v1.1.2
github.com/knadh/koanf/v2 v2.1.2
github.com/knadh/koanf/parsers/yaml v1.0.0
github.com/knadh/koanf/providers/file v1.2.0
github.com/knadh/koanf/v2 v2.2.0
github.com/leonelquinteros/gotext v1.7.1
github.com/lib/pq v1.10.9
github.com/libdns/alidns v1.0.3
github.com/libdns/cloudflare v0.1.1
github.com/libdns/huaweicloud v0.3.1
github.com/libdns/libdns v0.2.2
github.com/libdns/alidns v1.0.4
github.com/libdns/cloudflare v0.1.3
github.com/libdns/cloudns v1.0.0
github.com/libdns/duckdns v0.2.0
github.com/libdns/gcore v0.0.0-20250127070537-4a9d185c9d20
github.com/libdns/godaddy v1.0.3
github.com/libdns/hetzner v0.0.1
github.com/libdns/huaweicloud v0.3.5
github.com/libdns/libdns v0.2.3
github.com/libdns/linode v0.4.1
github.com/libdns/namecheap v0.0.0-20250228022813-d8b4b66c5072
github.com/libdns/namedotcom v0.3.3
github.com/libdns/namesilo v0.1.2
github.com/libdns/porkbun v0.2.0
github.com/libdns/tencentcloud v1.2.0
github.com/mholt/acmez/v3 v3.0.1
github.com/libdns/vercel v0.0.2
github.com/mholt/acmez/v3 v3.1.2
github.com/ncruces/go-sqlite3 v0.25.2
github.com/ncruces/go-sqlite3/gormlite v0.24.0
github.com/orandin/slog-gorm v1.4.0
github.com/pquerna/otp v1.4.1-0.20241231231527-5971b1ef1d66
github.com/robfig/cron/v3 v3.0.1
github.com/samber/lo v1.49.1
github.com/sethvargo/go-limiter v1.0.0
github.com/samber/lo v1.50.0
github.com/sethvargo/go-limiter v1.0.1-0.20250412144437-fa26982c7e1a
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/spf13/cast v1.7.1
github.com/spf13/cast v1.8.0
github.com/stretchr/testify v1.10.0
github.com/tufanbarisyildirim/gonginx v0.0.0-20250120210832-12a9c7ae0c8a
github.com/urfave/cli/v3 v3.0.0-beta1
golang.org/x/crypto v0.33.0
golang.org/x/net v0.35.0
github.com/tufanbarisyildirim/gonginx v0.0.0-20250429180229-7e931b1d4276
github.com/urfave/cli/v3 v3.3.3
golang.org/x/crypto v0.38.0
golang.org/x/net v0.40.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.12
gorm.io/gorm v1.26.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/G-Core/gcore-dns-sdk-go v0.2.9 // indirect
github.com/boombuler/barcode v1.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-rat/securecookie v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gofiber/schema v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gofiber/schema v1.3.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gookit/filter v1.2.2 // indirect
github.com/gookit/goutil v0.6.18 // indirect
github.com/imega/luaformatter v0.0.0-20211025140405-86b0a68d6bef // indirect
github.com/jaevor/go-nanoid v1.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/linode/linodego v1.23.0 // indirect
github.com/miekg/dns v1.1.40 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1096 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/timtadh/data-structures v0.5.3 // indirect
github.com/timtadh/lexmachine v0.2.2 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.9.0 // 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-20250210185358-939b2ce775ac // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
modernc.org/libc v1.61.13 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.8.2 // indirect
modernc.org/sqlite v1.34.5 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
replace (
github.com/libdns/tencentcloud => github.com/devhaozi/tencentcloud v0.0.0-20241129095955-7d4e898ce504
github.com/mholt/acmez/v3 => github.com/tnb-labs/acmez/v3 v3.0.0-20250101070450-67fb7b3d86d1
)
replace github.com/mholt/acmez/v3 => github.com/tnb-labs/acmez/v3 v3.0.0-20250416074048-0e9f6b5f8ec5
+411 -107
View File
@@ -1,70 +1,131 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
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/G-Core/gcore-dns-sdk-go v0.2.9 h1:LMMZIRX8y3aJJuAviNSpFmLbovZUw+6Om+8VElp1F90=
github.com/G-Core/gcore-dns-sdk-go v0.2.9/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4=
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/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.3.8 h1:QQ6KYgtnBbvYvCWuu/tOnBZamKAPtJzesj2qbjgyn7o=
github.com/bddjr/hlfhr v1.3.8/go.mod h1:oyIv4Q9JpCgZFdtH3KyTNWp7YYRWl4zl8k4ozrMAB4g=
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/boombuler/barcode v1.0.2 h1:79yrbttoZrLGkL/oOI8hBrUKucwOL0oOjUgEguGMcJ4=
github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/devhaozi/tencentcloud v0.0.0-20241129095955-7d4e898ce504 h1:Wk7RQyamGDfAz+5hZIT/GIm6ay5cORztgYVXOJTobKY=
github.com/devhaozi/tencentcloud v0.0.0-20241129095955-7d4e898ce504/go.mod h1:o0+WCxQ7LGLtyjnjYU4HbGW9uVjN44SdUDhxdUYLGPw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/devhaozi/westcn v0.0.0-20250407083704-b49375823872 h1:cns0Vj69pUuQ+bfS3KQrZyQycjZRxyPMiF3Njjz5Voc=
github.com/devhaozi/westcn v0.0.0-20250407083704-b49375823872/go.mod h1:3DfSvYwyUHt1JtN+JCWAGtOLFWlRXz19wJa/Xk159ao=
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/expr-lang/expr v1.17.2 h1:o0A99O/Px+/DTjEnQiodAgOIK9PPxL8DtXhBRKC+Iso=
github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-gormigrate/gormigrate/v2 v2.1.3 h1:ei3Vq/rpPI/jCJY9mRHJAKg5vU+EhZyWhBAkaAomQuw=
github.com/go-gormigrate/gormigrate/v2 v2.1.3/go.mod h1:VJ9FIOBAur+NmQ8c4tDVwOuiJcgupTG105FexPFrXzA=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gormigrate/gormigrate/v2 v2.1.4 h1:KOPEt27qy1cNzHfMZbp9YTmEuzkY4F4wrdsJW9WFk1U=
github.com/go-gormigrate/gormigrate/v2 v2.1.4/go.mod h1:y/6gPAH6QGAgP1UfHMiXcqGeJ88/GRQbfCReE1JJD5Y=
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-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-rat/chix v1.1.4 h1:YVDwF4vrlkm5hmlDGWKoiWPgP98pJooBvhsto8UWq+o=
github.com/go-rat/chix v1.1.4/go.mod h1:eKUTG7GXqMS278PH0QZi8yMWZddvMfxqWu7mverM6kQ=
github.com/go-rat/chix v1.2.0 h1:/pOkt2S1+VTfyS43EyZ35uVi8vssCPqQg1j/bORIk5M=
github.com/go-rat/chix v1.2.0/go.mod h1:YQP0YT0x/U3Xx5uQe2jfyMiS/bA732Nunnii6MG0wos=
github.com/go-rat/gormstore v1.0.6 h1:YxmaY6n3JoTk6o9RXoZ1+cQ6HQL+ayf/6d8D76ydCAc=
github.com/go-rat/gormstore v1.0.6/go.mod h1:xG/n+Du8baWf7ptiafiHLWBZWVXrYIPC42CsoEbfr4M=
github.com/go-rat/securecookie v1.1.0 h1:bvwE9n6X3a4eazOIevEvZghkvZ3OpY4BX1cnH/1/x0s=
github.com/go-rat/securecookie v1.1.0/go.mod h1:Z870TcL7ktAjRuhPeQXYuQcA6/DdzKwwqSwDQuPmufI=
github.com/go-rat/sessions v1.0.14 h1:YSzlGkKzsZEUIJj2s8ZEH4DcOqScm6htIVEOvH2lvd0=
github.com/go-rat/sessions v1.0.14/go.mod h1:jqpYqyMKZQl239GQ5W5LFRZUUQMGoNX9w7+zTQA7Soc=
github.com/go-rat/utils v1.0.9 h1:16zq6CpeULJgoW+TaJZ+gS4EPl0tQYAcuAdx3/GNCNs=
github.com/go-rat/utils v1.0.9/go.mod h1:LhzbrdsTHIo6Rjlz27Nh8edkugmoxbZjN87iV+ch+5M=
github.com/go-rat/sessions v1.1.0 h1:5zCySVVxNE6xLymf5F2u4TQPKIM7kz1MuD4TwIb/7Do=
github.com/go-rat/sessions v1.1.0/go.mod h1:ofnsWh7JoAApzCfIGd60xEyT5HlJY+Nx8zpWcwUXSHc=
github.com/go-rat/utils v1.1.5 h1:nB5qNU4TxqG9BJe7PdeDzznmh6lk+GIWb7hZcvAY4FQ=
github.com/go-rat/utils v1.1.5/go.mod h1:SUy4Xw44VkZvo2FaURgiIiJlL6Kukll85fVKMp/50+8=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU=
github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg=
github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
github.com/gofiber/schema v1.3.0 h1:K3F3wYzAY+aivfCCEHPufCthu5/13r/lzp1nuk6mr3Q=
github.com/gofiber/schema v1.3.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
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-cz/httplog v0.0.0-20241002114323-98e09d6f537a h1:BAyyIK6rc6Tq9vT2KVlPJXD8EVv/LpaFwn4fgfLwtHg=
github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a/go.mod h1:bgk4Ij/0OQ89UeoFFAQrSNhbbr4rKJ0fwWfo7wc+TCc=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI=
github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gookit/filter v1.2.2 h1:LSBQLk4M4fpfhaOG0hJ/GJ+qu+2+lLddgxlGfzcd8VQ=
@@ -73,192 +134,435 @@ github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw=
github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA=
github.com/gookit/validate v1.5.4 h1:nwBo6vULnVUeNFCOde6RKFRbOCKJXVMnWR0ghedacLg=
github.com/gookit/validate v1.5.4/go.mod h1:p9sRPfpvYB4vXICBpEPzv8FoAky+XhUOhWQghgmmat4=
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/imega/luaformatter v0.0.0-20211025140405-86b0a68d6bef h1:RC993DdTIHNItsyLj79fgZNLzrf9tBN0GR6W5ZPms6s=
github.com/imega/luaformatter v0.0.0-20211025140405-86b0a68d6bef/go.mod h1:i2XCfvmO94HrEOQWllihhtPrkvNfuB2R2p/o6+OVnRU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jaevor/go-nanoid v1.4.0 h1:mPz0oi3CrQyEtRxeRq927HHtZCJAAtZ7zdy7vOkrvWs=
github.com/jaevor/go-nanoid v1.4.0/go.mod h1:GIpPtsvl3eSBsjjIEFQdzzgpi50+Bo1Luk+aYlbJzlc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w=
github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=
github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/yaml v1.0.0 h1:PXyeHCRhAMKyfLJaoTWsqUTxIFeDMmdAKz3XVEslZV4=
github.com/knadh/koanf/parsers/yaml v1.0.0/go.mod h1:Q63VAOh/s6XaQs6a0TB2w9GFUuuPGvfYrCSWb9eWAQU=
github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U=
github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
github.com/knadh/koanf/v2 v2.2.0 h1:FZFwd9bUjpb8DyCWARUBy5ovuhDs1lI87dOEn2K8UVU=
github.com/knadh/koanf/v2 v2.2.0/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leonelquinteros/gotext v1.7.1 h1:/JNPeE3lY5JeVYv2+KBpz39994W3W9fmZCGq3eO9Ri8=
github.com/leonelquinteros/gotext v1.7.1/go.mod h1:I0WoFDn9u2D3VbPnnDPT8mzZu0iSXG8iih+AH2fHHqg=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ=
github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE=
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
github.com/libdns/huaweicloud v0.3.1 h1:wro0zpG86JKL3QVEaV/xfHW/59rmqwMYhcbl16w9EYg=
github.com/libdns/huaweicloud v0.3.1/go.mod h1:6s5ZIwLUr2qKsQz2SRNo7uh8X5uJohxoBR95rtSpNcI=
github.com/libdns/alidns v1.0.4 h1:Rc3Yy2SzMoho+3q3+fNy9vOVr2h9dcL8OLTgNKgxYbU=
github.com/libdns/alidns v1.0.4/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE=
github.com/libdns/cloudflare v0.1.3 h1:XPFa2f3Mm/3FDNwl9Ki2bfAQJ0Cm5GQB0e8PQVy25Us=
github.com/libdns/cloudflare v0.1.3/go.mod h1:XbvSCSMcxspwpSialM3bq0LsS3/Houy9WYxW8Ok8b6M=
github.com/libdns/cloudns v1.0.0 h1:Qrf8TepFnXVhQJi+IwCa/TXhaDy0RXZIXf+DGD/bcE4=
github.com/libdns/cloudns v1.0.0/go.mod h1:3bx4a2XxtgXcaDzTHktQqPNC4rghXAcSDd+GNPkhvjs=
github.com/libdns/duckdns v0.2.0 h1:vd3pE09G2qTx1Zh1o3LmrivWSByD3Z5FbL7csX5vDgE=
github.com/libdns/duckdns v0.2.0/go.mod h1:jCQ/7+qvhLK39+28qXvKEYGBBvmHBCmIwNqdJTCUmVs=
github.com/libdns/gcore v0.0.0-20250127070537-4a9d185c9d20 h1:bQwFw+C9sX/zYZlV53ey0KnNkxrfWYIFpvptuAVhJ1Y=
github.com/libdns/gcore v0.0.0-20250127070537-4a9d185c9d20/go.mod h1:JGoT1mbmqQwtYQqN5F/vGc9j4TTTMKw/hDm5vXADHUI=
github.com/libdns/godaddy v1.0.3 h1:PX1FOYDQ1HGQzz8mVOmtwm3aa6Sv5MwCkNzivUUTA44=
github.com/libdns/godaddy v1.0.3/go.mod h1:vuKWUXnvblDvcaiRwutOoLl7DuB21x8tI06owsF/JTM=
github.com/libdns/hetzner v0.0.1 h1:WsmcsOKnfpKmzwhfyqhGQEIlEeEaEUvb7ezoJgBKaqU=
github.com/libdns/hetzner v0.0.1/go.mod h1:Jj12aJipO9Ir7OGaXueJ5J1RnerFMD0auGa6k9kujG4=
github.com/libdns/huaweicloud v0.3.5 h1:TXv0QKkqgzEMSfqNUbAhoBj/2nrsiu1W6R0lKKH2vfc=
github.com/libdns/huaweicloud v0.3.5/go.mod h1:fadizEwwxdO/+Owy5VVdnpm7rMRrbo/B9ofBAH5UN5M=
github.com/libdns/libdns v0.1.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/libdns/libdns v0.2.3 h1:ba30K4ObwMGB/QTmqUxf3H4/GmUrCAIkMWejeGl12v8=
github.com/libdns/libdns v0.2.3/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/linode v0.4.1 h1:Fr22y3aWBzaj3AlFbCQ743eC+5Hss+sJ+OEjtlK61AA=
github.com/libdns/linode v0.4.1/go.mod h1:BqAJnQxzpnQgy4GDVYf3hQvhLGDc3n8BRwMZ0/9m2y0=
github.com/libdns/namecheap v0.0.0-20250228022813-d8b4b66c5072 h1:hCPOoLd9Mr5kAWLofhSCuGYZxAVVy0CV9zgbahzIeSg=
github.com/libdns/namecheap v0.0.0-20250228022813-d8b4b66c5072/go.mod h1:jo2LWZSD/g/scevxNSp2kWtdMIXfsgWxrtSc3Z0Yy/I=
github.com/libdns/namedotcom v0.3.3 h1:R10C7+IqQGVeC4opHHMiFNBxdNBg1bi65ZwqLESl+jE=
github.com/libdns/namedotcom v0.3.3/go.mod h1:GbYzsAF2yRUpI0WgIK5fs5UX+kDVUPaYCFLpTnKQm0s=
github.com/libdns/namesilo v0.1.2 h1:ySW4QiWx2vMWw1xBD7kTnTWVY/CLIaCgko6ZbmOufbk=
github.com/libdns/namesilo v0.1.2/go.mod h1:Mk+6XAOCWYaHF3V/oMVP6g96qpoQtj2PzrSz2NpKq4Q=
github.com/libdns/porkbun v0.2.0 h1:oa2F0doE93RiJkauVVbM+P2AZ7jovDSiH6u3aaQezvQ=
github.com/libdns/porkbun v0.2.0/go.mod h1:mwrhwXpsSA2Xw23t9+qmgFnz+erkn25Sxuh7IBA+x2I=
github.com/libdns/tencentcloud v1.2.0 h1:SBbZ9gUZ+ba/p2d8NpiOcj+WZTdYqB5Fz7zypdbK3YU=
github.com/libdns/tencentcloud v1.2.0/go.mod h1:o0+WCxQ7LGLtyjnjYU4HbGW9uVjN44SdUDhxdUYLGPw=
github.com/libdns/vercel v0.0.2 h1:slfQWxgUdYFgKfPbK1vLE7k1CZSspHFlwC+nEWlm79w=
github.com/libdns/vercel v0.0.2/go.mod h1:z2NBq/OQ8G0JPglZmit0YnqHm/k+eOz8s4XyLvYxhQo=
github.com/linode/linodego v1.23.0 h1:s0ReCZtuN9Z1IoUN9w1RLeYO1dMZUGPwOQ/IBFsBHtU=
github.com/linode/linodego v1.23.0/go.mod h1:0U7wj/UQOqBNbKv1FYTXiBUXueR8DY4HvIotwE0ENgg=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/ncruces/go-sqlite3 v0.25.2 h1:suu3C7y92hPqozqO8+w3K333Q1VhWyN6K3JJKXdtC2U=
github.com/ncruces/go-sqlite3 v0.25.2/go.mod h1:46HIzeCQQ+aNleAxCli+vpA2tfh7ttSnw24kQahBc1o=
github.com/ncruces/go-sqlite3/gormlite v0.24.0 h1:81sHeq3CCdhjoqAB650n5wEdRlLO9VBvosArskcN3+c=
github.com/ncruces/go-sqlite3/gormlite v0.24.0/go.mod h1:vXfVWdBfg7qOgqQqHpzUWl9LLswD0h+8mK4oouaV2oc=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU=
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/otp v1.4.1-0.20241231231527-5971b1ef1d66 h1:7pccJSb6/BRgd5BKejX/dgzinfq0K5HPwj35+saHuCU=
github.com/pquerna/otp v1.4.1-0.20241231231527-5971b1ef1d66/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4=
github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY=
github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sethvargo/go-limiter v1.0.1-0.20250412144437-fa26982c7e1a h1:CdCoDHVynJVAQWN7ZQrAUOp0SV5TmRwNOSkF5KedDko=
github.com/sethvargo/go-limiter v1.0.1-0.20250412144437-fa26982c7e1a/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
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=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1096 h1:DMokC7T0UF8wMfT1kD+mX3M+hc2C06gmFvQ9gsfRPmI=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1096/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/timtadh/data-structures v0.5.3 h1:F2tEjoG9qWIyUjbvXVgJqEOGJPMIiYn7U5W5mE+i/vQ=
github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU=
github.com/timtadh/lexmachine v0.2.2 h1:g55RnjdYazm5wnKv59pwFcBJHOyvTPfDEoz21s4PHmY=
github.com/timtadh/lexmachine v0.2.2/go.mod h1:GBJvD5OAfRn/gnp92zb9KTgHLB7akKyxmVivoYCcjQI=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/tnb-labs/acmez/v3 v3.0.0-20250101070450-67fb7b3d86d1 h1:7IhZYEiBWy3KfKi9wZkv4Um8QYYF2fYzQ+xNrOx/tHI=
github.com/tnb-labs/acmez/v3 v3.0.0-20250101070450-67fb7b3d86d1/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/tufanbarisyildirim/gonginx v0.0.0-20250120210832-12a9c7ae0c8a h1:bFMBmB409YhHd+B4yfM7JPYTCgxnQjqzIfFOiN4Tpsc=
github.com/tufanbarisyildirim/gonginx v0.0.0-20250120210832-12a9c7ae0c8a/go.mod h1:hdMWBc1+TyB6G5ZZBBgPWQ8cjRZ6zpYdhal0uu6E9QM=
github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tnb-labs/acmez/v3 v3.0.0-20250416074048-0e9f6b5f8ec5 h1:eeJt90c4buPPC4JjfP5EaTFwlARgOaaXqewHFqZJJag=
github.com/tnb-labs/acmez/v3 v3.0.0-20250416074048-0e9f6b5f8ec5/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/tufanbarisyildirim/gonginx v0.0.0-20250429180229-7e931b1d4276 h1:928VpvLiMWSulfjhKUYj57f70HhOE1ImsQHaeDC2jVc=
github.com/tufanbarisyildirim/gonginx v0.0.0-20250429180229-7e931b1d4276/go.mod h1:ALbEe81QPWOZjDKCKNWodG2iqCMtregG8+ebQgjx2+4=
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
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.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
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-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
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=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
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=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/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-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw=
gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0=
modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo=
modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw=
modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8=
modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E=
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.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI=
modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
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=
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=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+2 -2
View File
@@ -2,10 +2,10 @@ package app
import (
"context"
"fmt"
"os"
"github.com/go-gormigrate/gormigrate/v2"
"github.com/gookit/color"
"github.com/urfave/cli/v3"
"github.com/tnb-labs/panel/pkg/apploader"
@@ -30,7 +30,7 @@ func (r *Cli) Run() error {
_ = r.migrator.Migrate()
if err := r.cmd.Run(context.TODO(), os.Args); err != nil {
fmt.Printf("|-%v\n", err)
color.Errorf("|-%v\n", err)
}
return nil
+4 -4
View File
@@ -3,12 +3,13 @@ package apps
import (
"github.com/google/wire"
"github.com/tnb-labs/panel/internal/apps/benchmark"
"github.com/tnb-labs/panel/internal/apps/codeserver"
"github.com/tnb-labs/panel/internal/apps/docker"
"github.com/tnb-labs/panel/internal/apps/fail2ban"
"github.com/tnb-labs/panel/internal/apps/frp"
"github.com/tnb-labs/panel/internal/apps/gitea"
"github.com/tnb-labs/panel/internal/apps/memcached"
"github.com/tnb-labs/panel/internal/apps/minio"
"github.com/tnb-labs/panel/internal/apps/mysql"
"github.com/tnb-labs/panel/internal/apps/nginx"
"github.com/tnb-labs/panel/internal/apps/php74"
@@ -25,16 +26,16 @@ import (
"github.com/tnb-labs/panel/internal/apps/rsync"
"github.com/tnb-labs/panel/internal/apps/s3fs"
"github.com/tnb-labs/panel/internal/apps/supervisor"
"github.com/tnb-labs/panel/internal/apps/toolbox"
)
var ProviderSet = wire.NewSet(
benchmark.NewApp,
codeserver.NewApp,
docker.NewApp,
fail2ban.NewApp,
frp.NewApp,
gitea.NewApp,
memcached.NewApp,
minio.NewApp,
mysql.NewApp,
nginx.NewApp,
php74.NewApp,
@@ -51,5 +52,4 @@ var ProviderSet = wire.NewSet(
rsync.NewApp,
s3fs.NewApp,
supervisor.NewApp,
toolbox.NewApp,
)
-691
View File
@@ -1,691 +0,0 @@
package benchmark
import (
"bytes"
"compress/gzip"
"crypto/aes"
"crypto/cipher"
cryptorand "crypto/rand"
"encoding/json"
"fmt"
"image"
"image/color"
"io"
"math"
"math/big"
"math/rand/v2"
"net/http"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/go-chi/chi/v5"
"github.com/tnb-labs/panel/internal/service"
)
type App struct {
}
func NewApp() *App {
return &App{}
}
func (s *App) Route(r chi.Router) {
r.Post("/test", s.Test)
}
// Test 运行测试
func (s *App) Test(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Test](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
switch req.Name {
case "image":
result := s.imageProcessing(req.Multi)
service.Success(w, result)
case "machine":
result := s.machineLearning(req.Multi)
service.Success(w, result)
case "compile":
result := s.compileSimulationSingle(req.Multi)
service.Success(w, result)
case "encryption":
result := s.encryptionTest(req.Multi)
service.Success(w, result)
case "compression":
result := s.compressionTest(req.Multi)
service.Success(w, result)
case "physics":
result := s.physicsSimulation(req.Multi)
service.Success(w, result)
case "json":
result := s.jsonProcessing(req.Multi)
service.Success(w, result)
case "disk":
result := s.diskTestTask()
service.Success(w, result)
case "memory":
result := s.memoryTestTask()
service.Success(w, result)
default:
service.Error(w, http.StatusUnprocessableEntity, "未知测试类型")
}
}
// calculateCpuScore 计算CPU成绩
func (s *App) calculateCpuScore(duration time.Duration) int {
score := int((10 / duration.Seconds()) * float64(3000))
if score < 0 {
score = 0
}
return score
}
// calculateScore 计算内存/硬盘成绩
func (s *App) calculateScore(duration time.Duration) int {
score := int((20 / duration.Seconds()) * float64(30000))
if score < 0 {
score = 0
}
return score
}
// 图像处理
func (s *App) imageProcessing(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
if err := s.imageProcessingTask(n); err != nil {
return 0
}
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *App) imageProcessingTask(numThreads int) error {
img := image.NewRGBA(image.Rect(0, 0, 4000, 4000))
for x := 0; x < 4000; x++ {
for y := 0; y < 4000; y++ {
img.Set(x, y, color.RGBA{R: uint8(x % 256), G: uint8(y % 256), A: 255})
}
}
var wg sync.WaitGroup
dx := img.Bounds().Dx()
dy := img.Bounds().Dy()
chunkSize := dy / numThreads
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
startY := i * chunkSize
endY := startY + chunkSize
if i == numThreads-1 {
endY = dy
}
for x := 1; x < dx-1; x++ {
for y := startY + 1; y < endY-1; y++ {
// 卷积操作(模糊)
rTotal, gTotal, bTotal := 0, 0, 0
for k := -1; k <= 1; k++ {
for l := -1; l <= 1; l++ {
r, g, b, _ := img.At(x+k, y+l).RGBA()
rTotal += int(r)
gTotal += int(g)
bTotal += int(b)
}
}
rAvg := uint8(rTotal / 9 / 256)
gAvg := uint8(gTotal / 9 / 256)
bAvg := uint8(bTotal / 9 / 256)
img.Set(x, y, color.RGBA{R: rAvg, G: gAvg, B: bAvg, A: 255})
}
}
}(i)
}
wg.Wait()
return nil
}
// 机器学习(矩阵乘法)
func (s *App) machineLearning(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.machineLearningTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *App) machineLearningTask(numThreads int) {
size := 900
a := make([][]float64, size)
b := make([][]float64, size)
for i := 0; i < size; i++ {
a[i] = make([]float64, size)
b[i] = make([]float64, size)
for j := 0; j < size; j++ {
a[i][j] = rand.Float64()
b[i][j] = rand.Float64()
}
}
c := make([][]float64, size)
for i := 0; i < size; i++ {
c[i] = make([]float64, size)
}
var wg sync.WaitGroup
chunkSize := size / numThreads
for k := 0; k < numThreads; k++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
start := k * chunkSize
end := start + chunkSize
if k == numThreads-1 {
end = size
}
for i := start; i < end; i++ {
for j := 0; j < size; j++ {
sum := 0.0
for l := 0; l < size; l++ {
sum += a[i][l] * b[l][j]
}
c[i][j] = sum
}
}
}(k)
}
wg.Wait()
}
// 数学问题(计算斐波那契数)
func (s *App) compileSimulationSingle(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
totalCalculations := 1000
fibNumber := 20000
calculationsPerThread := totalCalculations / n
remainder := totalCalculations % n
var wg sync.WaitGroup
for i := 0; i < n; i++ {
tasks := calculationsPerThread
if i < remainder {
tasks++ // 处理无法均分的剩余任务
}
wg.Add(1)
go func(tasks int) {
defer wg.Done()
for j := 0; j < tasks; j++ {
s.fib(fibNumber)
}
}(tasks)
}
wg.Wait()
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
// 斐波那契函数
func (s *App) fib(n int) *big.Int {
if n < 2 {
return big.NewInt(int64(n))
}
a := big.NewInt(0)
b := big.NewInt(1)
temp := big.NewInt(0)
for i := 2; i <= n; i++ {
temp.Add(a, b)
a.Set(b)
b.Set(temp)
}
return b
}
// AES加密
func (s *App) encryptionTest(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
if err := s.encryptionTestTask(n); err != nil {
return 0
}
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *App) encryptionTestTask(numThreads int) error {
key := []byte("abcdefghijklmnopqrstuvwxyz123456")
dataSize := 1024 * 1024 * 512 // 512 MB
plaintext := []byte(strings.Repeat("A", dataSize))
block, err := aes.NewCipher(key)
if err != nil {
return err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return err
}
chunkSize := dataSize / numThreads
var wg sync.WaitGroup
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
start := i * chunkSize
end := start + chunkSize
if i == numThreads-1 {
end = dataSize
}
nonce := make([]byte, aesGCM.NonceSize())
if _, err = cryptorand.Read(nonce); err != nil {
return
}
aesGCM.Seal(nil, nonce, plaintext[start:end], nil)
}(i)
}
wg.Wait()
return nil
}
// 压缩/解压缩
func (s *App) compressionTest(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.compressionTestTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *App) compressionTestTask(numThreads int) {
data := []byte(strings.Repeat("耗子面板", 50000000))
chunkSize := len(data) / numThreads
var wg sync.WaitGroup
compressedChunks := make([]bytes.Buffer, numThreads)
// 压缩
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
start := i * chunkSize
end := start + chunkSize
if i == numThreads-1 {
end = len(data)
}
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
_, _ = w.Write(data[start:end])
_ = w.Close()
compressedChunks[i] = buf
}(i)
}
wg.Wait()
// 解压缩
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
r, err := gzip.NewReader(&compressedChunks[i])
if err != nil {
return
}
_, err = io.Copy(io.Discard, r)
if err != nil {
return
}
_ = r.Close()
}(i)
}
wg.Wait()
}
// 物理仿真(N体问题)
func (s *App) physicsSimulation(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.physicsSimulationTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *App) physicsSimulationTask(numThreads int) {
const (
numBodies = 4000
steps = 30
)
type Body struct {
x, y, z, vx, vy, vz float64
}
bodies := make([]Body, numBodies)
for i := 0; i < numBodies; i++ {
bodies[i] = Body{
x: rand.Float64(),
y: rand.Float64(),
z: rand.Float64(),
vx: rand.Float64(),
vy: rand.Float64(),
vz: rand.Float64(),
}
}
chunkSize := numBodies / numThreads
for step := 0; step < steps; step++ {
var wg sync.WaitGroup
// 更新速度
for k := 0; k < numThreads; k++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
start := k * chunkSize
end := start + chunkSize
if k == numThreads-1 {
end = numBodies
}
for i := start; i < end; i++ {
bi := &bodies[i]
for j := 0; j < numBodies; j++ {
if i == j {
continue
}
bj := &bodies[j]
dx := bj.x - bi.x
dy := bj.y - bi.y
dz := bj.z - bi.z
dist := math.Sqrt(dx*dx + dy*dy + dz*dz)
if dist == 0 {
continue
}
force := 1 / (dist * dist)
bi.vx += force * dx / dist
bi.vy += force * dy / dist
bi.vz += force * dz / dist
}
}
}(k)
}
wg.Wait()
// 更新位置
for k := 0; k < numThreads; k++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
start := k * chunkSize
end := start + chunkSize
if k == numThreads-1 {
end = numBodies
}
for i := start; i < end; i++ {
bi := &bodies[i]
bi.x += bi.vx
bi.y += bi.vy
bi.z += bi.vz
}
}(k)
}
wg.Wait()
}
}
// JSON解析
func (s *App) jsonProcessing(multi bool) int {
n := 1
if multi {
n = runtime.NumCPU()
}
start := time.Now()
s.jsonProcessingTask(n)
duration := time.Since(start)
return s.calculateCpuScore(duration)
}
func (s *App) jsonProcessingTask(numThreads int) {
numElements := 1000000
elementsPerThread := numElements / numThreads
var wg sync.WaitGroup
for i := 0; i < numThreads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
start := i * elementsPerThread
end := start + elementsPerThread
if i == numThreads-1 {
end = numElements
}
elements := make([]map[string]any, 0, end-start)
for j := start; j < end; j++ {
elements = append(elements, map[string]any{
"id": j,
"value": fmt.Sprintf("Value%d", j),
})
}
encoded, err := json.Marshal(elements)
if err != nil {
return
}
var parsed []map[string]any
err = json.Unmarshal(encoded, &parsed)
if err != nil {
return
}
}(i)
}
wg.Wait()
}
// 内存性能
func (s *App) memoryTestTask() map[string]any {
results := make(map[string]any)
dataSize := 500 * 1024 * 1024 // 500 MB
data := make([]byte, dataSize)
_, _ = cryptorand.Read(data)
start := time.Now()
// 内存读写速度
results["bandwidth"] = s.memoryBandwidthTest(data)
// 内存访问延迟
data = data[:100*1024*1024] // 100 MB
results["latency"] = s.memoryLatencyTest(data)
duration := time.Since(start)
results["score"] = s.calculateScore(duration)
return results
}
func (s *App) memoryBandwidthTest(data []byte) string {
dataSize := len(data)
startTime := time.Now()
for i := 0; i < dataSize; i++ {
data[i] ^= 0xFF
}
duration := time.Since(startTime).Seconds()
if duration == 0 {
return "N/A"
}
speed := float64(dataSize) / duration / (1024 * 1024)
return fmt.Sprintf("%.2f MB/s", speed)
}
func (s *App) memoryLatencyTest(data []byte) string {
dataSize := len(data)
indices := rand.Perm(dataSize)
startTime := time.Now()
sum := byte(0)
for _, idx := range indices {
sum ^= data[idx]
}
duration := time.Since(startTime).Seconds()
if duration == 0 {
return "N/A"
}
avgLatency := duration * 1e9 / float64(dataSize)
return fmt.Sprintf("%.2f ns", avgLatency)
}
// 硬盘IO
func (s *App) diskTestTask() map[string]any {
results := make(map[string]any)
blockSizes := []int64{4 * 1024, 64 * 1024, 512 * 1024, 1 * 1024 * 1024} // 4K, 64K, 512K, 1M
fileSize := int64(100 * 1024 * 1024) // 100MB 文件
start := time.Now()
for _, blockSize := range blockSizes {
result := s.diskIOTest(blockSize, fileSize)
results[fmt.Sprintf("%d", blockSize/1024)] = result
}
duration := time.Since(start)
results["score"] = s.calculateScore(duration)
return results
}
func (s *App) diskIOTest(blockSize int64, fileSize int64) map[string]any {
result := make(map[string]any)
tempFile := fmt.Sprintf("tempfile_%d", blockSize)
defer os.Remove(tempFile)
// 写测试
writeSpeed, writeIOPS := s.diskWriteTest(tempFile, blockSize, fileSize)
// 读测试
readSpeed, readIOPS := s.diskReadTest(tempFile, blockSize, fileSize)
result["write_speed"] = fmt.Sprintf("%.2f MB/s", writeSpeed)
result["write_iops"] = fmt.Sprintf("%.2f IOPS", writeIOPS)
result["read_speed"] = fmt.Sprintf("%.2f MB/s", readSpeed)
result["read_iops"] = fmt.Sprintf("%.2f IOPS", readIOPS)
return result
}
func (s *App) diskWriteTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
totalBlocks := fileSize / blockSize
data := make([]byte, blockSize)
_, _ = cryptorand.Read(data)
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_SYNC, 0644)
if err != nil {
return 0, 0
}
defer file.Close()
start := time.Now()
for i := int64(0); i < totalBlocks; i++ {
// 生成随机偏移
offset := rand.Int64N(fileSize - blockSize + 1)
_, err := file.WriteAt(data, offset)
if err != nil {
return 0, 0
}
}
_ = file.Sync()
duration := time.Since(start).Seconds()
if duration == 0 {
duration = 1
}
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
iops := float64(totalBlocks) / duration
return speed, iops
}
func (s *App) diskReadTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
totalBlocks := fileSize / blockSize
data := make([]byte, blockSize)
file, err := os.OpenFile(fileName, os.O_RDONLY|os.O_SYNC, 0644)
if err != nil {
return 0, 0
}
defer file.Close()
start := time.Now()
for i := int64(0); i < totalBlocks; i++ {
// 生成随机偏移
offset := rand.Int64N(fileSize - blockSize + 1)
_, err := file.ReadAt(data, offset)
if err != nil && err != io.EOF {
return 0, 0
}
}
duration := time.Since(start).Seconds()
if duration == 0 {
duration = 1
}
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
iops := float64(totalBlocks) / duration
return speed, iops
}
-6
View File
@@ -1,6 +0,0 @@
package benchmark
type Test struct {
Name string `json:"name" validate:"required|in:image,machine,compile,encryption,compression,physics,json,memory,disk"`
Multi bool `json:"multi"`
}
+47
View File
@@ -0,0 +1,47 @@
package codeserver
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/tnb-labs/panel/internal/service"
"github.com/tnb-labs/panel/pkg/io"
"github.com/tnb-labs/panel/pkg/systemctl"
)
type App struct{}
func NewApp() *App {
return &App{}
}
func (s *App) Route(r chi.Router) {
r.Get("/config", s.GetConfig)
r.Post("/config", s.UpdateConfig)
}
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
config, _ := io.Read("/root/.config/code-server/config.yaml")
service.Success(w, config)
}
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[UpdateConfig](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = io.Write("/root/.config/code-server/config.yaml", req.Config, 0600); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = systemctl.Restart("code-server"); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}
+5
View File
@@ -0,0 +1,5 @@
package codeserver
type UpdateConfig struct {
Config string `form:"config" json:"config" validate:"required"`
}
+27 -28
View File
@@ -9,6 +9,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-rat/chix"
"github.com/go-rat/utils/str"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/internal/app"
@@ -19,11 +20,13 @@ import (
)
type App struct {
t *gotext.Locale
websiteRepo biz.WebsiteRepo
}
func NewApp(website biz.WebsiteRepo) *App {
func NewApp(t *gotext.Locale, website biz.WebsiteRepo) *App {
return &App{
t: t,
websiteRepo: website,
}
}
@@ -34,8 +37,8 @@ func (s *App) Route(r chi.Router) {
r.Delete("/jails", s.Delete)
r.Get("/jails/{name}", s.BanList)
r.Post("/unban", s.Unban)
r.Post("/whiteList", s.SetWhiteList)
r.Get("/whiteList", s.GetWhiteList)
r.Post("/white_list", s.SetWhiteList)
r.Get("/white_list", s.GetWhiteList)
}
// List 所有规则
@@ -47,12 +50,8 @@ func (s *App) List(w http.ResponseWriter, r *http.Request) {
}
jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1)
if len(jailList) == 0 {
service.Error(w, http.StatusUnprocessableEntity, "Fail2ban 规则为空")
return
}
var jails []Jail
jails := make([]Jail, 0)
for i, jail := range jailList {
if i == 0 {
continue
@@ -107,7 +106,7 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
return
}
if (strings.Contains(raw, "["+jailName+"]") && jailType == "service") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-cc") && jailType == "website" && jailWebsiteMode == "cc") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-path") && jailType == "website" && jailWebsiteMode == "path") {
service.Error(w, http.StatusUnprocessableEntity, "规则已存在")
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("rule already exists"))
return
}
@@ -115,7 +114,7 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
case "website":
website, err := s.websiteRepo.GetByName(jailWebsiteName)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "获取网站配置失败:%v", err)
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
var ports string
@@ -140,7 +139,7 @@ logpath = ` + app.Root + `/wwwlogs/` + website.Name + `.log
`
raw += rule
if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入Fail2ban规则失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -159,7 +158,7 @@ ignoreregex =
`
}
if err = io.Write("/etc/fail2ban/filter.d/haozi-"+jailWebsiteName+"-"+jailWebsiteMode+".conf", filter, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入Fail2ban规则失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -178,11 +177,11 @@ ignoreregex =
filter = "pure-ftpd"
port, err = shell.Execf(`cat %s/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`, app.Root)
default:
service.Error(w, http.StatusUnprocessableEntity, "未知服务")
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("unknown service"))
return
}
if len(port) == 0 || err != nil {
service.Error(w, http.StatusUnprocessableEntity, "获取服务端口失败,请检查是否安装")
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("get service port failed, please check if it is installed"))
return
}
@@ -199,13 +198,13 @@ bantime = ` + jailBanTime + `
`
raw += rule
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入Fail2ban规则失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
}
if _, err = shell.Execf("fail2ban-client reload"); err != nil {
service.Error(w, http.StatusInternalServerError, "重载配置失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -226,20 +225,20 @@ func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if !strings.Contains(raw, "["+req.Name+"]") {
service.Error(w, http.StatusUnprocessableEntity, "规则不存在")
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("rule not found"))
return
}
rule := str.Cut(raw, "# "+req.Name+"-START", "# "+req.Name+"-END")
raw = strings.Replace(raw, "\n# "+req.Name+"-START"+rule+"# "+req.Name+"-END", "", -1)
raw = strings.ReplaceAll(raw, "\n# "+req.Name+"-START"+rule+"# "+req.Name+"-END", "")
raw = strings.TrimSpace(raw)
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入Fail2ban规则失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
service.Error(w, http.StatusInternalServerError, "重载配置失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -256,17 +255,17 @@ func (s *App) BanList(w http.ResponseWriter, r *http.Request) {
currentlyBan, err := shell.Execf(`fail2ban-client status %s | grep "Currently banned" | awk '{print $4}'`, req.Name)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取封禁列表失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get current banned list"))
return
}
totalBan, err := shell.Execf(`fail2ban-client status %s | grep "Total banned" | awk '{print $4}'`, req.Name)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取封禁列表失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get total banned list"))
return
}
bannedIp, err := shell.Execf(`fail2ban-client status %s | grep "Banned IP list" | awk -F ":" '{print $2}'`, req.Name)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取封禁列表失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get banned ip list"))
return
}
bannedIpList := strings.Split(bannedIp, " ")
@@ -300,7 +299,7 @@ func (s *App) Unban(w http.ResponseWriter, r *http.Request) {
}
if _, err = shell.Execf("fail2ban-client set %s unbanip %s", req.Name, req.IP); err != nil {
service.Error(w, http.StatusInternalServerError, "解封失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -325,17 +324,17 @@ func (s *App) SetWhiteList(w http.ResponseWriter, r *http.Request) {
if reg.MatchString(raw) {
raw = reg.ReplaceAllString(raw, "ignoreip = "+req.IP+"\n")
} else {
service.Error(w, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to parse the ignoreip of fail2ban"))
return
}
if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入Fail2ban规则失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if _, err = shell.Execf("fail2ban-client reload"); err != nil {
service.Error(w, http.StatusInternalServerError, "重载配置失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
@@ -353,7 +352,7 @@ func (s *App) GetWhiteList(w http.ResponseWriter, r *http.Request) {
ignoreIp := reg.FindStringSubmatch(raw)[1]
service.Success(w, ignoreIp)
} else {
service.Error(w, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to parse the ignoreip of fail2ban"))
return
}
}
+14 -7
View File
@@ -7,6 +7,7 @@ import (
"regexp"
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/service"
"github.com/tnb-labs/panel/pkg/io"
@@ -14,10 +15,14 @@ import (
"github.com/tnb-labs/panel/pkg/types"
)
type App struct{}
type App struct {
t *gotext.Locale
}
func NewApp() *App {
return &App{}
func NewApp(t *gotext.Locale) *App {
return &App{
t: t,
}
}
func (s *App) Route(r chi.Router) {
@@ -29,7 +34,7 @@ func (s *App) Route(r chi.Router) {
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
status, err := systemctl.Status("memcached")
if err != nil {
service.Error(w, http.StatusInternalServerError, "failed to get Memcached status: %v", err)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get Memcached status: %v", err))
return
}
if !status {
@@ -42,11 +47,13 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
service.Success(w, []types.NV{})
return
}
defer conn.Close()
defer func(conn net.Conn) {
_ = conn.Close()
}(conn)
_, err = conn.Write([]byte("stats\nquit\n"))
if err != nil {
service.Error(w, http.StatusInternalServerError, "failed to write to Memcached: %v", err)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to write to Memcached: %v", err))
return
}
@@ -67,7 +74,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
}
if err = scanner.Err(); err != nil {
service.Error(w, http.StatusInternalServerError, "failed to read from Memcached: %v", err)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to read from Memcached: %v", err))
return
}
+47
View File
@@ -0,0 +1,47 @@
package minio
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/tnb-labs/panel/internal/service"
"github.com/tnb-labs/panel/pkg/io"
"github.com/tnb-labs/panel/pkg/systemctl"
)
type App struct{}
func NewApp() *App {
return &App{}
}
func (s *App) Route(r chi.Router) {
r.Get("/env", s.GetEnv)
r.Post("/env", s.UpdateEnv)
}
func (s *App) GetEnv(w http.ResponseWriter, r *http.Request) {
env, _ := io.Read("/etc/default/minio")
service.Success(w, env)
}
func (s *App) UpdateEnv(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[UpdateEnv](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = io.Write("/etc/default/minio", req.Env, 0600); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = systemctl.Restart("minio"); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}
+5
View File
@@ -0,0 +1,5 @@
package minio
type UpdateEnv struct {
Env string `form:"env" json:"env" validate:"required"`
}
+42 -41
View File
@@ -7,6 +7,7 @@ import (
"regexp"
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/internal/app"
@@ -21,11 +22,13 @@ import (
)
type App struct {
t *gotext.Locale
settingRepo biz.SettingRepo
}
func NewApp(setting biz.SettingRepo) *App {
func NewApp(t *gotext.Locale, setting biz.SettingRepo) *App {
return &App{
t: t,
settingRepo: setting,
}
}
@@ -34,18 +37,18 @@ func (s *App) Route(r chi.Router) {
r.Get("/load", s.Load)
r.Get("/config", s.GetConfig)
r.Post("/config", s.UpdateConfig)
r.Post("/clearErrorLog", s.ClearErrorLog)
r.Get("/slowLog", s.SlowLog)
r.Post("/clearSlowLog", s.ClearSlowLog)
r.Get("/rootPassword", s.GetRootPassword)
r.Post("/rootPassword", s.SetRootPassword)
r.Post("/clear_error_log", s.ClearErrorLog)
r.Get("/slow_log", s.SlowLog)
r.Post("/clear_slow_log", s.ClearSlowLog)
r.Get("/root_password", s.GetRootPassword)
r.Post("/root_password", s.SetRootPassword)
}
// GetConfig 获取配置
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
config, err := io.Read(app.Root + "/server/mysql/conf/my.cnf")
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取配置失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -61,12 +64,12 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
}
if err = io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入配置失败:%v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = systemctl.Restart("mysqld"); err != nil {
service.Error(w, http.StatusInternalServerError, "重启失败:%v", err)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to restart MySQL: %v", err))
return
}
@@ -77,12 +80,12 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
rootPassword, err := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取root密码失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to load MySQL root password: %v", err))
return
}
if len(rootPassword) == 0 {
service.Error(w, http.StatusUnprocessableEntity, "root密码为空")
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("MySQL root password is empty"))
return
}
@@ -93,12 +96,12 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
}
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
service.Error(w, http.StatusInternalServerError, "设置环境变量失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set MYSQL_PWD env: %v", err))
return
}
raw, err := shell.Execf(`mysqladmin -u root extended-status`)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取负载失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get MySQL status: %v", err))
return
}
_ = os.Unsetenv("MYSQL_PWD")
@@ -108,24 +111,24 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
regex string
name string
}{
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到硬盘"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
{`Uptime\s+\|\s+(\d+)\s+\|`, s.t.Get("Uptime")},
{`Queries\s+\|\s+(\d+)\s+\|`, s.t.Get("Total Queries")},
{`Connections\s+\|\s+(\d+)\s+\|`, s.t.Get("Total Connections")},
{`Com_commit\s+\|\s+(\d+)\s+\|`, s.t.Get("Transactions per Second")},
{`Com_rollback\s+\|\s+(\d+)\s+\|`, s.t.Get("Rollbacks per Second")},
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, s.t.Get("Bytes Sent")},
{`Bytes_received\s+\|\s+(\d+)\s+\|`, s.t.Get("Bytes Received")},
{`Threads_connected\s+\|\s+(\d+)\s+\|`, s.t.Get("Active Connections")},
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, s.t.Get("Peak Connections")},
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, s.t.Get("Index Hit Rate")},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, s.t.Get("Innodb Index Hit Rate")},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, s.t.Get("Temporary Tables Created on Disk")},
{`Open_tables\s+\|\s+(\d+)\s+\|`, s.t.Get("Open Tables")},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, s.t.Get("Full Joins without Index")},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, s.t.Get("Full Range Joins without Index")},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, s.t.Get("Subqueries without Index")},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, s.t.Get("Sort Merge Passes")},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, s.t.Get("Table Locks Waited")},
}
for _, expression := range expressions {
@@ -133,7 +136,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
matches := re.FindStringSubmatch(raw)
if len(matches) > 1 {
d := map[string]string{"name": expression.name, "value": matches[1]}
if expression.name == "发送" || expression.name == "接收" {
if expression.name == s.t.Get("Bytes Sent") || expression.name == s.t.Get("Bytes Received") {
d["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
}
@@ -155,7 +158,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
// ClearErrorLog 清空错误日志
func (s *App) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
if err := systemctl.LogsClear("mysqld"); err != nil {
if err := systemctl.LogClear("mysqld"); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -170,7 +173,7 @@ func (s *App) SlowLog(w http.ResponseWriter, r *http.Request) {
// ClearSlowLog 清空慢查询日志
func (s *App) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
if _, err := shell.Execf("echo '' > %s/server/mysql/mysql-slow.log", app.Root); err != nil {
if _, err := shell.Execf("cat /dev/null > %s/server/mysql/mysql-slow.log", app.Root); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -182,11 +185,7 @@ func (s *App) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
func (s *App) GetRootPassword(w http.ResponseWriter, r *http.Request) {
rootPassword, err := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取root密码失败")
return
}
if len(rootPassword) == 0 {
service.Error(w, http.StatusUnprocessableEntity, "root密码为空")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to load MySQL root password: %v", err))
return
}
@@ -210,14 +209,16 @@ func (s *App) SetRootPassword(w http.ResponseWriter, r *http.Request) {
return
}
} else {
defer mysql.Close()
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
if err = mysql.UserPassword("root", req.Password, "localhost"); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
}
if err = s.settingRepo.Set(biz.SettingKeyMySQLRootPassword, req.Password); err != nil {
service.Error(w, http.StatusInternalServerError, "设置保存失败:%v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
+24 -20
View File
@@ -8,6 +8,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-resty/resty/v2"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/internal/app"
@@ -20,25 +21,27 @@ import (
)
type App struct {
// Dependent services
t *gotext.Locale
}
func NewApp() *App {
return &App{}
func NewApp(t *gotext.Locale) *App {
return &App{
t: t,
}
}
func (s *App) Route(r chi.Router) {
r.Get("/load", s.Load)
r.Get("/config", s.GetConfig)
r.Post("/config", s.SaveConfig)
r.Get("/errorLog", s.ErrorLog)
r.Post("/clearErrorLog", s.ClearErrorLog)
r.Get("/error_log", s.ErrorLog)
r.Post("/clear_error_log", s.ClearErrorLog)
}
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
config, err := io.Read(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root))
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取配置失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -53,13 +56,13 @@ func (s *App) SaveConfig(w http.ResponseWriter, r *http.Request) {
}
if err = io.Write(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root), req.Config, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "保存配置失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
service.Error(w, http.StatusInternalServerError, "重载服务失败: %v", err)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload nginx: %v", err))
return
}
@@ -71,7 +74,7 @@ func (s *App) ErrorLog(w http.ResponseWriter, r *http.Request) {
}
func (s *App) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
if _, err := shell.Execf("echo '' > %s/%s", app.Root, "wwwlogs/nginx-error.log"); err != nil {
if _, err := shell.Execf("cat /dev/null > %s/%s", app.Root, "wwwlogs/nginx-error.log"); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -84,6 +87,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
resp, err := client.R().Get("http://127.0.0.1/nginx_status")
if err != nil || !resp.IsSuccess() {
service.Success(w, []types.NV{})
return
}
raw := resp.String()
@@ -91,29 +95,29 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
workers, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | wc -l")
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取负载失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get nginx workers: %v", err))
return
}
data = append(data, types.NV{
Name: "工作进程",
Name: s.t.Get("Workers"),
Value: workers,
})
out, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取负载失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get nginx workers: %v", err))
return
}
mem := tools.FormatBytes(cast.ToFloat64(out))
data = append(data, types.NV{
Name: "内存占用",
Name: s.t.Get("Memory"),
Value: mem,
})
match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 2 {
data = append(data, types.NV{
Name: "活跃连接数",
Name: s.t.Get("Active connections"),
Value: match[1],
})
}
@@ -121,15 +125,15 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
match = regexp.MustCompile(`server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 4 {
data = append(data, types.NV{
Name: "总连接次数",
Name: s.t.Get("Total connections"),
Value: match[1],
})
data = append(data, types.NV{
Name: "总握手次数",
Name: s.t.Get("Total handshakes"),
Value: match[2],
})
data = append(data, types.NV{
Name: "总请求次数",
Name: s.t.Get("Total requests"),
Value: match[3],
})
}
@@ -137,15 +141,15 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
match = regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 4 {
data = append(data, types.NV{
Name: "请求数",
Name: s.t.Get("Reading"),
Value: match[1],
})
data = append(data, types.NV{
Name: "响应数",
Name: s.t.Get("Writing"),
Value: match[2],
})
data = append(data, types.NV{
Name: "驻留进程",
Name: s.t.Get("Waiting"),
Value: match[3],
})
}
+232 -143
View File
@@ -10,6 +10,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-resty/resty/v2"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/internal/app"
@@ -22,11 +23,13 @@ import (
type App struct {
version uint
t *gotext.Locale
taskRepo biz.TaskRepo
}
func NewApp(task biz.TaskRepo) *App {
func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
return &App{
t: t,
taskRepo: task,
}
}
@@ -35,17 +38,18 @@ func (s *App) Route(version uint) func(r chi.Router) {
return func(r chi.Router) {
php := new(App)
php.version = version
php.t = s.t
php.taskRepo = s.taskRepo
r.Post("/setCli", php.SetCli)
r.Post("/set_cli", php.SetCli)
r.Get("/config", php.GetConfig)
r.Post("/config", php.UpdateConfig)
r.Get("/fpmConfig", php.GetFPMConfig)
r.Post("/fpmConfig", php.UpdateFPMConfig)
r.Get("/fpm_config", php.GetFPMConfig)
r.Post("/fpm_config", php.UpdateFPMConfig)
r.Get("/load", php.Load)
r.Get("/errorLog", php.ErrorLog)
r.Get("/slowLog", php.SlowLog)
r.Post("/clearErrorLog", php.ClearErrorLog)
r.Post("/clearSlowLog", php.ClearSlowLog)
r.Get("/error_log", php.ErrorLog)
r.Get("/slow_log", php.SlowLog)
r.Post("/clear_error_log", php.ClearErrorLog)
r.Post("/clear_slow_log", php.ClearSlowLog)
r.Get("/extensions", php.ExtensionList)
r.Post("/extensions", php.InstallExtension)
r.Delete("/extensions", php.UninstallExtension)
@@ -120,8 +124,36 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
return
}
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
rawKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
dataKeys := []string{
s.t.Get("Application Pool"),
s.t.Get("Process Manager"),
s.t.Get("Start Time"),
s.t.Get("Accepted Connections"),
s.t.Get("Listen Queue"),
s.t.Get("Max Listen Queue"),
s.t.Get("Listen Queue Length"),
s.t.Get("Idle Processes"),
s.t.Get("Active Processes"),
s.t.Get("Total Processes"),
s.t.Get("Max Active Processes"),
s.t.Get("Max Children Reached"),
s.t.Get("Slow Requests"),
}
rawKeys := []string{
"pool",
"process manager",
"start time",
"accepted conn",
"listen queue",
"max listen queue",
"listen queue len",
"idle processes",
"active processes",
"total processes",
"max active processes",
"max children reached",
"slow requests",
}
loads := make([]types.NV, 0)
for i := range dataKeys {
@@ -146,7 +178,7 @@ func (s *App) SlowLog(w http.ResponseWriter, r *http.Request) {
}
func (s *App) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
if _, err := shell.Execf("echo '' > %s/server/php/%d/var/log/php-fpm.log", app.Root, s.version); err != nil {
if _, err := shell.Execf("cat /dev/null > %s/server/php/%d/var/log/php-fpm.log", app.Root, s.version); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -155,7 +187,7 @@ func (s *App) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
}
func (s *App) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
if _, err := shell.Execf("echo '' > %s/server/php/%d/var/log/slow.log", app.Root, s.version); err != nil {
if _, err := shell.Execf("cat /dev/null > %s/server/php/%d/var/log/slow.log", app.Root, s.version); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -194,7 +226,7 @@ func (s *App) InstallExtension(w http.ResponseWriter, r *http.Request) {
}
if !s.checkExtension(req.Slug) {
service.Error(w, http.StatusUnprocessableEntity, "扩展不存在")
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("extension %s does not exist", req.Slug))
return
}
@@ -205,7 +237,7 @@ func (s *App) InstallExtension(w http.ResponseWriter, r *http.Request) {
}
task := new(biz.Task)
task.Name = fmt.Sprintf("安装PHP-%d扩展 %s", s.version, req.Slug)
task.Name = s.t.Get("Install PHP-%d %s extension", s.version, req.Slug)
task.Status = biz.TaskStatusWaiting
task.Shell = cmd
task.Log = "/tmp/" + req.Slug + ".log"
@@ -225,7 +257,7 @@ func (s *App) UninstallExtension(w http.ResponseWriter, r *http.Request) {
}
if !s.checkExtension(req.Slug) {
service.Error(w, http.StatusUnprocessableEntity, "扩展不存在")
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("extension %s does not exist", req.Slug))
return
}
@@ -236,7 +268,7 @@ func (s *App) UninstallExtension(w http.ResponseWriter, r *http.Request) {
}
task := new(biz.Task)
task.Name = fmt.Sprintf("卸载PHP-%d扩展 %s", s.version, req.Slug)
task.Name = s.t.Get("Uninstall PHP-%d %s extension", s.version, req.Slug)
task.Status = biz.TaskStatusWaiting
task.Shell = cmd
task.Log = "/tmp/" + req.Slug + ".log"
@@ -253,149 +285,206 @@ func (s *App) getExtensions() []Extension {
{
Name: "fileinfo",
Slug: "fileinfo",
Description: "Fileinfo 是一个用于识别文件类型的库",
Description: s.t.Get("Fileinfo is a library used to identify file types"),
},
{
Name: "OPcache",
Slug: "Zend OPcache",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销",
},
{
Name: "Redis",
Slug: "redis",
Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据(需先安装 igbinary 拓展)",
},
{
Name: "Memcached",
Slug: "memcached",
Description: "Memcached 使用 libmemcached 库连接 Memcached 服务器",
},
{
Name: "ImageMagick",
Slug: "imagick",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件",
},
{
Name: "exif",
Slug: "exif",
Description: "通过 exif 扩展,您可以操作图像元数据",
},
{
Name: "pgsql",
Slug: "pgsql",
Description: "pgsql 是一个驱动程序,它启用从 PHP 到 PostgreSQL 数据库的访问(需先安装 PostgreSQL 应用)",
},
{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: "pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问(需先安装 PostgreSQL 应用)",
},
{
Name: "imap",
Slug: "imap",
Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件",
},
{
Name: "zip",
Slug: "zip",
Description: "Zip 是一个用于处理 ZIP 文件的库",
},
{
Name: "bz2",
Slug: "bz2",
Description: "Bzip2 是一个用于压缩和解压缩文件的库",
},
{
Name: "readline",
Slug: "readline",
Description: "Readline 是一个库,它提供了一种用于处理文本的接口",
},
{
Name: "snmp",
Slug: "snmp",
Description: "SNMP 是一种用于网络管理的协议",
},
{
Name: "ldap",
Slug: "ldap",
Description: "LDAP 是一种用于访问目录服务的协议",
},
{
Name: "enchant",
Slug: "enchant",
Description: "Enchant 是一个拼写检查库",
},
{
Name: "pspell",
Slug: "pspell",
Description: "Pspell 是一个拼写检查库",
},
{
Name: "calendar",
Slug: "calendar",
Description: "Calendar 是一个用于处理日期的库",
},
{
Name: "gmp",
Slug: "gmp",
Description: "GMP 是一个用于处理大整数的库",
},
{
Name: "sysvmsg",
Slug: "sysvmsg",
Description: "Sysvmsg 是一个用于处理 System V 消息队列的库",
},
{
Name: "sysvsem",
Slug: "sysvsem",
Description: "Sysvsem 是一个用于处理 System V 信号量的库",
},
{
Name: "sysvshm",
Slug: "sysvshm",
Description: "Sysvshm 是一个用于处理 System V 共享内存的库",
},
{
Name: "xsl",
Slug: "xsl",
Description: "XSL 是一个用于处理 XML 文档的库",
},
{
Name: "intl",
Slug: "intl",
Description: "Intl 是一个用于处理国际化和本地化的库",
},
{
Name: "gettext",
Slug: "gettext",
Description: "Gettext 是一个用于处理多语言的库",
Description: s.t.Get("OPcache stores precompiled PHP script bytecode in shared memory to improve PHP performance"),
},
{
Name: "igbinary",
Slug: "igbinary",
Description: "Igbinary 是一个用于序列化和反序列化数据的库",
Description: s.t.Get("Igbinary is a library for serializing and deserializing data"),
},
{
Name: "Redis",
Slug: "redis",
Description: s.t.Get("PhpRedis connects to and operates on data in Redis databases (requires the igbinary extension installed above)"),
},
{
Name: "Memcached",
Slug: "memcached",
Description: s.t.Get("Memcached is a driver for connecting to Memcached servers"),
},
{
Name: "ImageMagick",
Slug: "imagick",
Description: s.t.Get("ImageMagick is free software for creating, editing, and composing images"),
},
{
Name: "exif",
Slug: "exif",
Description: s.t.Get("Exif is a library for reading and writing image metadata"),
},
{
Name: "pgsql",
Slug: "pgsql",
Description: s.t.Get("pgsql is a driver for connecting to PostgreSQL (requires PostgreSQL installed)"),
},
{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: s.t.Get("pdo_pgsql is a PDO driver for connecting to PostgreSQL (requires PostgreSQL installed)"),
},
{
Name: "sqlsrv",
Slug: "sqlsrv",
Description: s.t.Get("sqlsrv is a driver for connecting to SQL Server"),
},
{
Name: "pdo_sqlsrv",
Slug: "pdo_sqlsrv",
Description: s.t.Get("pdo_sqlsrv is a PDO driver for connecting to SQL Server"),
},
{
Name: "imap",
Slug: "imap",
Description: s.t.Get("IMAP extension allows PHP to read, search, delete, download, and manage emails"),
},
{
Name: "zip",
Slug: "zip",
Description: s.t.Get("Zip is a library for handling ZIP files"),
},
{
Name: "bz2",
Slug: "bz2",
Description: s.t.Get("Bzip2 is a library for compressing and decompressing files"),
},
{
Name: "ssh2",
Slug: "ssh2",
Description: s.t.Get("SSH2 is a library for connecting to SSH servers"),
},
{
Name: "event",
Slug: "event",
Description: s.t.Get("Event is a library for handling events"),
},
{
Name: "readline",
Slug: "readline",
Description: s.t.Get("Readline is a library for processing text"),
},
{
Name: "snmp",
Slug: "snmp",
Description: s.t.Get("SNMP is a protocol for network management"),
},
{
Name: "ldap",
Slug: "ldap",
Description: s.t.Get("LDAP is a protocol for accessing directory services"),
},
{
Name: "enchant",
Slug: "enchant",
Description: s.t.Get("Enchant is a spell-checking library"),
},
{
Name: "pspell",
Slug: "pspell",
Description: s.t.Get("Pspell is a spell-checking library"),
},
{
Name: "calendar",
Slug: "calendar",
Description: s.t.Get("Calendar is a library for handling dates"),
},
{
Name: "gmp",
Slug: "gmp",
Description: s.t.Get("GMP is a library for handling large integers"),
},
{
Name: "xlswriter",
Slug: "xlswriter",
Description: s.t.Get("XLSWriter is a high-performance library for reading and writing Excel files"),
},
{
Name: "xsl",
Slug: "xsl",
Description: s.t.Get("XSL is a library for processing XML documents"),
},
{
Name: "intl",
Slug: "intl",
Description: s.t.Get("Intl is a library for handling internationalization and localization"),
},
{
Name: "gettext",
Slug: "gettext",
Description: s.t.Get("Gettext is a library for handling multilingual support"),
},
{
Name: "grpc",
Slug: "grpc",
Description: s.t.Get("gRPC is a high-performance, open-source, and general-purpose RPC framework"),
},
{
Name: "protobuf",
Slug: "protobuf",
Description: s.t.Get("protobuf is a library for serializing and deserializing data"),
},
{
Name: "rdkafka",
Slug: "rdkafka",
Description: s.t.Get("rdkafka is a library for connecting to Apache Kafka"),
},
{
Name: "xhprof",
Slug: "xhprof",
Description: s.t.Get("xhprof is a library for performance profiling"),
},
{
Name: "xdebug",
Slug: "xdebug",
Description: s.t.Get("xdebug is a library for debugging and profiling PHP code"),
},
{
Name: "yaml",
Slug: "yaml",
Description: s.t.Get("yaml is a library for handling YAML"),
},
{
Name: "zstd",
Slug: "zstd",
Description: s.t.Get("zstd is a library for compressing and decompressing files"),
},
{
Name: "sysvmsg",
Slug: "sysvmsg",
Description: s.t.Get("Sysvmsg is a library for handling System V message queues"),
},
{
Name: "sysvsem",
Slug: "sysvsem",
Description: s.t.Get("Sysvsem is a library for handling System V semaphores"),
},
{
Name: "sysvshm",
Slug: "sysvshm",
Description: s.t.Get("Sysvshm is a library for handling System V shared memory"),
},
{
Name: "ionCube",
Slug: "ionCube Loader",
Description: s.t.Get("ionCube is a professional-grade PHP encryption and decryption tool (must be installed after OPcache)"),
},
{
Name: "Swoole",
Slug: "swoole",
Description: s.t.Get("Swoole is a PHP extension for building high-performance asynchronous concurrent servers"),
},
}
// ionCube Swoole 不支持 PHP 8.4
if cast.ToUint(s.version) < 84 {
extensions = append(extensions, Extension{
Name: "ionCube",
Slug: "ionCube Loader",
Description: "ionCube 是一个专业级的 PHP 加密解密工具(需在 OPcache 之后安装)",
})
extensions = append(extensions, Extension{
Name: "Swoole",
Slug: "swoole",
Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展",
})
}
// Swow 不支持 PHP 8.0 以下版本且目前不支持 PHP 8.4
if cast.ToUint(s.version) >= 80 && cast.ToUint(s.version) < 84 {
extensions = append(extensions, Extension{
Name: "Swow",
Slug: "Swow",
Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展",
Description: s.t.Get("Swow is a PHP extension for building high-performance asynchronous concurrent servers"),
})
}
// PHP 8.4 移除了 pspell 和 imap 并且不再建议使用
+3 -2
View File
@@ -2,6 +2,7 @@ package php74
import (
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/apps/php"
"github.com/tnb-labs/panel/internal/biz"
@@ -11,9 +12,9 @@ type App struct {
php *php.App
}
func NewApp(task biz.TaskRepo) *App {
func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
return &App{
php: php.NewApp(task),
php: php.NewApp(t, task),
}
}
+3 -2
View File
@@ -2,6 +2,7 @@ package php80
import (
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/apps/php"
"github.com/tnb-labs/panel/internal/biz"
@@ -11,9 +12,9 @@ type App struct {
php *php.App
}
func NewApp(task biz.TaskRepo) *App {
func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
return &App{
php: php.NewApp(task),
php: php.NewApp(t, task),
}
}
+3 -2
View File
@@ -2,6 +2,7 @@ package php81
import (
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/apps/php"
"github.com/tnb-labs/panel/internal/biz"
@@ -11,9 +12,9 @@ type App struct {
php *php.App
}
func NewApp(task biz.TaskRepo) *App {
func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
return &App{
php: php.NewApp(task),
php: php.NewApp(t, task),
}
}
+3 -2
View File
@@ -2,6 +2,7 @@ package php82
import (
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/apps/php"
"github.com/tnb-labs/panel/internal/biz"
@@ -11,9 +12,9 @@ type App struct {
php *php.App
}
func NewApp(task biz.TaskRepo) *App {
func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
return &App{
php: php.NewApp(task),
php: php.NewApp(t, task),
}
}
+3 -2
View File
@@ -2,6 +2,7 @@ package php83
import (
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/apps/php"
"github.com/tnb-labs/panel/internal/biz"
@@ -11,9 +12,9 @@ type App struct {
php *php.App
}
func NewApp(task biz.TaskRepo) *App {
func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
return &App{
php: php.NewApp(task),
php: php.NewApp(t, task),
}
}
+3 -2
View File
@@ -2,6 +2,7 @@ package php84
import (
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/apps/php"
"github.com/tnb-labs/panel/internal/biz"
@@ -11,9 +12,9 @@ type App struct {
php *php.App
}
func NewApp(task biz.TaskRepo) *App {
func NewApp(t *gotext.Locale, task biz.TaskRepo) *App {
return &App{
php: php.NewApp(task),
php: php.NewApp(t, task),
}
}
+14 -9
View File
@@ -9,6 +9,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-rat/chix"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/internal/app"
@@ -19,10 +20,14 @@ import (
"github.com/tnb-labs/panel/pkg/systemctl"
)
type App struct{}
type App struct {
t *gotext.Locale
}
func NewApp() *App {
return &App{}
func NewApp(t *gotext.Locale) *App {
return &App{
t: t,
}
}
func (s *App) Route(r chi.Router) {
@@ -35,7 +40,7 @@ func (s *App) Route(r chi.Router) {
func (s *App) Info(w http.ResponseWriter, r *http.Request) {
files, err := os.ReadDir(fmt.Sprintf("%s/server/phpmyadmin", app.Root))
if err != nil {
service.Error(w, http.StatusInternalServerError, "找不到 phpMyAdmin 目录")
service.Error(w, http.StatusInternalServerError, s.t.Get("phpMyAdmin directory not found"))
return
}
@@ -46,7 +51,7 @@ func (s *App) Info(w http.ResponseWriter, r *http.Request) {
}
}
if len(phpmyadmin) == 0 {
service.Error(w, http.StatusInternalServerError, "找不到 phpMyAdmin 目录")
service.Error(w, http.StatusInternalServerError, s.t.Get("phpMyAdmin directory not found"))
return
}
@@ -57,7 +62,7 @@ func (s *App) Info(w http.ResponseWriter, r *http.Request) {
}
match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf)
if len(match) == 0 {
service.Error(w, http.StatusInternalServerError, "找不到 phpMyAdmin 端口")
service.Error(w, http.StatusInternalServerError, s.t.Get("phpMyAdmin port not found"))
return
}
@@ -81,7 +86,7 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
}
conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+cast.ToString(req.Port)+";")
if err = io.Write(fmt.Sprintf("%s/server/vhost/phpmyadmin.conf", app.Root), conf, 0644); err != nil {
service.ErrorSystem(w)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -100,7 +105,7 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
service.Error(w, http.StatusInternalServerError, "重载OpenResty失败:%v", err)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload nginx: %v", err))
return
}
@@ -131,7 +136,7 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
if err = systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
service.Error(w, http.StatusInternalServerError, "重载OpenResty失败:%v", err)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload nginx: %v", err))
return
}
+4 -4
View File
@@ -17,10 +17,10 @@ func NewApp() *App {
}
func (s *App) Route(r chi.Router) {
r.Get("/registryConfig", s.GetRegistryConfig)
r.Post("/registryConfig", s.UpdateRegistryConfig)
r.Get("/storageConfig", s.GetStorageConfig)
r.Post("/storageConfig", s.UpdateStorageConfig)
r.Get("/registry_config", s.GetRegistryConfig)
r.Post("/registry_config", s.UpdateRegistryConfig)
r.Get("/storage_config", s.GetStorageConfig)
r.Post("/storage_config", s.UpdateStorageConfig)
}
func (s *App) GetRegistryConfig(w http.ResponseWriter, r *http.Request) {
+27 -22
View File
@@ -6,6 +6,7 @@ import (
"time"
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/app"
"github.com/tnb-labs/panel/internal/service"
@@ -15,20 +16,24 @@ import (
"github.com/tnb-labs/panel/pkg/types"
)
type App struct{}
type App struct {
t *gotext.Locale
}
func NewApp() *App {
return &App{}
func NewApp(t *gotext.Locale) *App {
return &App{
t: t,
}
}
func (s *App) Route(r chi.Router) {
r.Get("/config", s.GetConfig)
r.Post("/config", s.UpdateConfig)
r.Get("/userConfig", s.GetUserConfig)
r.Post("/userConfig", s.UpdateUserConfig)
r.Get("/user_config", s.GetUserConfig)
r.Post("/user_config", s.UpdateUserConfig)
r.Get("/load", s.Load)
r.Get("/log", s.Log)
r.Post("/clearLog", s.ClearLog)
r.Post("/clear_log", s.ClearLog)
}
// GetConfig 获取配置
@@ -36,7 +41,7 @@ func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
// 获取配置
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root))
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL配置失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -52,12 +57,12 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
}
if err = io.Write(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root), req.Config, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入PostgreSQL配置失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = systemctl.Reload("postgresql"); err != nil {
service.Error(w, http.StatusInternalServerError, "重载服务失败: %v", err)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload PostgreSQL: %v", err))
return
}
@@ -69,7 +74,7 @@ func (s *App) GetUserConfig(w http.ResponseWriter, r *http.Request) {
// 获取配置
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root))
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL配置失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -85,12 +90,12 @@ func (s *App) UpdateUserConfig(w http.ResponseWriter, r *http.Request) {
}
if err = io.Write(fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), req.Config, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入PostgreSQL配置失败")
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = systemctl.Reload("postgresql"); err != nil {
service.Error(w, http.StatusInternalServerError, "重载服务失败: %v", err)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload PostgreSQL: %v", err))
return
}
@@ -107,36 +112,36 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
start, err := shell.Execf(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL启动时间失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL start time: %v", err))
return
}
pid, err := shell.Execf(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL进程PID失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL backend pid: %v", err))
return
}
process, err := shell.Execf(`ps aux | grep postgres | grep -v grep | wc -l`)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL进程数失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL process: %v", err))
return
}
connections, err := shell.Execf(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL连接数失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL connections: %v", err))
return
}
storage, err := shell.Execf(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取PostgreSQL空间占用失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL database size: %v", err))
return
}
data := []types.NV{
{Name: "启动时间", Value: start},
{Name: "进程 PID", Value: pid},
{Name: "进程数", Value: process},
{Name: "总连接数", Value: connections},
{Name: "空间占用", Value: storage},
{Name: s.t.Get("Start Time"), Value: start},
{Name: s.t.Get("Process PID"), Value: pid},
{Name: s.t.Get("Process Count"), Value: process},
{Name: s.t.Get("Total Connections"), Value: connections},
{Name: s.t.Get("Storage Usage"), Value: storage},
}
service.Success(w, data)
+12 -7
View File
@@ -7,6 +7,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-rat/chix"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/internal/app"
@@ -17,10 +18,14 @@ import (
"github.com/tnb-labs/panel/pkg/systemctl"
)
type App struct{}
type App struct {
t *gotext.Locale
}
func NewApp() *App {
return &App{}
func NewApp(t *gotext.Locale) *App {
return &App{
t: t,
}
}
func (s *App) Route(r chi.Router) {
@@ -76,16 +81,16 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
req.Path = "/" + req.Path
}
if !io.Exists(req.Path) {
service.Error(w, http.StatusUnprocessableEntity, "目录不存在")
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("directory %s does not exist", req.Path))
return
}
if err = io.Chmod(req.Path, 0755); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "修改目录权限失败")
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = io.Chown(req.Path, "www", "www"); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "修改目录权限失败")
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if _, err = shell.Execf(`yes '%s' | pure-pw useradd '%s' -u www -g www -d '%s'`, req.Password, req.Username, req.Path); err != nil {
@@ -144,7 +149,7 @@ func (s *App) ChangePassword(w http.ResponseWriter, r *http.Request) {
func (s *App) GetPort(w http.ResponseWriter, r *http.Request) {
port, err := shell.Execf(`cat %s/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`, app.Root)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取PureFtpd端口失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get port: %v", err))
return
}
+24 -19
View File
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/go-chi/chi/v5"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/app"
"github.com/tnb-labs/panel/internal/service"
@@ -16,10 +17,14 @@ import (
"github.com/tnb-labs/panel/pkg/types"
)
type App struct{}
type App struct {
t *gotext.Locale
}
func NewApp() *App {
return &App{}
func NewApp(t *gotext.Locale) *App {
return &App{
t: t,
}
}
func (s *App) Route(r chi.Router) {
@@ -31,7 +36,7 @@ func (s *App) Route(r chi.Router) {
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
status, err := systemctl.Status("redis")
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取 Redis 状态失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get redis status: %v", err))
return
}
if !status {
@@ -46,7 +51,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
re := regexp.MustCompile(`requirepass\s+(.+)`)
re := regexp.MustCompile(`^requirepass\s+(.+)`)
matches := re.FindStringSubmatch(config)
if len(matches) == 2 {
withPassword = " -a " + matches[1]
@@ -54,7 +59,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
raw, err := shell.Execf("redis-cli%s info", withPassword)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取 Redis 负载失败")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get redis info: %v", err))
return
}
@@ -69,19 +74,19 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
}
data := []types.NV{
{Name: "TCP 端口", Value: dataRaw["tcp_port"]},
{Name: "已运行天数", Value: dataRaw["uptime_in_days"]},
{Name: "连接的客户端数", Value: dataRaw["connected_clients"]},
{Name: "已分配的内存总量", Value: dataRaw["used_memory_human"]},
{Name: "占用内存总量", Value: dataRaw["used_memory_rss_human"]},
{Name: "占用内存峰值", Value: dataRaw["used_memory_peak_human"]},
{Name: "内存碎片比率", Value: dataRaw["mem_fragmentation_ratio"]},
{Name: "运行以来连接过的客户端的总数", Value: dataRaw["total_connections_received"]},
{Name: "运行以来执行过的命令的总数", Value: dataRaw["total_commands_processed"]},
{Name: "每秒执行的命令数", Value: dataRaw["instantaneous_ops_per_sec"]},
{Name: "查找数据库键成功次数", Value: dataRaw["keyspace_hits"]},
{Name: "查找数据库键失败次数", Value: dataRaw["keyspace_misses"]},
{Name: "最近一次 fork() 操作耗费的毫秒数", Value: dataRaw["latest_fork_usec"]},
{Name: s.t.Get("TCP Port"), Value: dataRaw["tcp_port"]},
{Name: s.t.Get("Uptime in Days"), Value: dataRaw["uptime_in_days"]},
{Name: s.t.Get("Connected Clients"), Value: dataRaw["connected_clients"]},
{Name: s.t.Get("Total Allocated Memory"), Value: dataRaw["used_memory_human"]},
{Name: s.t.Get("Total Memory Usage"), Value: dataRaw["used_memory_rss_human"]},
{Name: s.t.Get("Peak Memory Usage"), Value: dataRaw["used_memory_peak_human"]},
{Name: s.t.Get("Memory Fragmentation Ratio"), Value: dataRaw["mem_fragmentation_ratio"]},
{Name: s.t.Get("Total Connections Received"), Value: dataRaw["total_connections_received"]},
{Name: s.t.Get("Total Commands Processed"), Value: dataRaw["total_commands_processed"]},
{Name: s.t.Get("Commands Per Second"), Value: dataRaw["instantaneous_ops_per_sec"]},
{Name: s.t.Get("Keyspace Hits"), Value: dataRaw["keyspace_hits"]},
{Name: s.t.Get("Keyspace Misses"), Value: dataRaw["keyspace_misses"]},
{Name: s.t.Get("Latest Fork Time (ms)"), Value: dataRaw["latest_fork_usec"]},
}
service.Success(w, data)
+14 -9
View File
@@ -9,6 +9,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-rat/chix"
"github.com/go-rat/utils/str"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/service"
"github.com/tnb-labs/panel/pkg/io"
@@ -16,10 +17,14 @@ import (
"github.com/tnb-labs/panel/pkg/systemctl"
)
type App struct{}
type App struct {
t *gotext.Locale
}
func NewApp() *App {
return &App{}
func NewApp(t *gotext.Locale) *App {
return &App{
t: t,
}
}
func (s *App) Route(r chi.Router) {
@@ -74,7 +79,7 @@ func (s *App) List(w http.ResponseWriter, r *http.Request) {
currentModule.AuthUser = value
currentModule.Secret, err = shell.Execf(`grep -E '^%s:.*$' /etc/rsyncd.secrets | awk -F ':' '{print $2}'`, currentModule.AuthUser)
if err != nil {
service.Error(w, http.StatusInternalServerError, "获取模块%s的密钥失败", currentModule.AuthUser)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get the secret key for module %s", currentModule.AuthUser))
return
}
case "hosts allow":
@@ -109,7 +114,7 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
return
}
if strings.Contains(config, "["+req.Name+"]") {
service.Error(w, http.StatusUnprocessableEntity, "模块%s已存在", req.Name)
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("module %s already exists", req.Name))
return
}
@@ -154,12 +159,12 @@ func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if !strings.Contains(config, "["+req.Name+"]") {
service.Error(w, http.StatusUnprocessableEntity, "模块%s不存在", req.Name)
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("module %s does not exist", req.Name))
return
}
module := str.Cut(config, "# "+req.Name+"-START", "# "+req.Name+"-END")
config = strings.Replace(config, "\n# "+req.Name+"-START"+module+"# "+req.Name+"-END", "", -1)
config = strings.ReplaceAll(config, "\n# "+req.Name+"-START"+module+"# "+req.Name+"-END", "")
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
if len(match) == 2 {
@@ -196,7 +201,7 @@ func (s *App) Update(w http.ResponseWriter, r *http.Request) {
return
}
if !strings.Contains(config, "["+req.Name+"]") {
service.Error(w, http.StatusUnprocessableEntity, "模块%s不存在", req.Name)
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("module %s does not exist", req.Name))
return
}
@@ -211,7 +216,7 @@ secrets file = /etc/rsyncd.secrets
# ` + req.Name + `-END`
module := str.Cut(config, "# "+req.Name+"-START", "# "+req.Name+"-END")
config = strings.Replace(config, "# "+req.Name+"-START"+module+"# "+req.Name+"-END", newConf, -1)
config = strings.ReplaceAll(config, "# "+req.Name+"-START"+module+"# "+req.Name+"-END", newConf)
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
if len(match) == 2 {
+82 -79
View File
@@ -1,29 +1,29 @@
package s3fs
import (
"encoding/json"
"net/http"
"os"
"regexp"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-rat/chix"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/internal/biz"
"github.com/tnb-labs/panel/internal/service"
"github.com/tnb-labs/panel/pkg/io"
"github.com/tnb-labs/panel/pkg/shell"
)
type App struct {
settingRepo biz.SettingRepo
t *gotext.Locale
}
func NewApp(setting biz.SettingRepo) *App {
func NewApp(t *gotext.Locale) *App {
return &App{
settingRepo: setting,
t: t,
}
}
@@ -35,19 +35,13 @@ func (s *App) Route(r chi.Router) {
// List 所有 S3fs 挂载
func (s *App) List(w http.ResponseWriter, r *http.Request) {
var s3fsList []Mount
list, err := s.settingRepo.Get("s3fs", "[]")
list, err := s.mounts()
if err != nil {
service.Error(w, http.StatusInternalServerError, "failed to get s3fs list")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get s3fs list: %v", err))
return
}
if err = json.Unmarshal([]byte(list), &s3fsList); err != nil {
service.Error(w, http.StatusInternalServerError, "failed to get s3fs list")
return
}
paged, total := service.Paginate(r, s3fsList)
paged, total := service.Paginate(r, list)
service.Success(w, chix.M{
"total": total,
@@ -65,36 +59,31 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
// 检查下地域节点中是否包含bucket,如果包含了,肯定是错误的
if strings.Contains(req.URL, req.Bucket) {
service.Error(w, http.StatusUnprocessableEntity, "endpoint should not contain bucket")
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("endpoint should not contain bucket"))
return
}
// 检查挂载目录是否存在且为空
if !io.Exists(req.Path) {
if err = os.MkdirAll(req.Path, 0755); err != nil {
service.Error(w, http.StatusUnprocessableEntity, "failed to create mount path: %v", err)
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("failed to create mount path: %v", err))
return
}
}
if !io.Empty(req.Path) {
service.Error(w, http.StatusUnprocessableEntity, "mount path is not empty")
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount path is not empty"))
return
}
var s3fsList []Mount
list, err := s.settingRepo.Get("s3fs", "[]")
list, err := s.mounts()
if err != nil {
service.Error(w, http.StatusInternalServerError, "failed to get s3fs list")
return
}
if err = json.Unmarshal([]byte(list), &s3fsList); err != nil {
service.Error(w, http.StatusInternalServerError, "failed to get s3fs list")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get s3fs list: %v", err))
return
}
for _, s := range s3fsList {
if s.Path == req.Path {
service.Error(w, http.StatusUnprocessableEntity, "mount path already exists")
for _, item := range list {
if item.Path == req.Path {
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount path already exists"))
return
}
}
@@ -102,7 +91,7 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
id := time.Now().UnixMicro()
password := req.Ak + ":" + req.Sk
if err = io.Write("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600); err != nil {
service.Error(w, http.StatusInternalServerError, "failed to create passwd file: %v", err)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to create passwd file: %v", err))
return
}
if _, err = shell.Execf(`echo 's3fs#%s %s fuse _netdev,allow_other,nonempty,url=%s,passwd_file=/etc/passwd-s3fs-%s 0 0' >> /etc/fstab`, req.Bucket, req.Path, req.URL, cast.ToString(id)); err != nil {
@@ -111,29 +100,12 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
}
if _, err = shell.Execf("mount -a"); err != nil {
_, _ = shell.Execf(`sed -i 's@^s3fs#%s\s%s.*$@@g' /etc/fstab`, req.Bucket, req.Path)
service.Error(w, http.StatusInternalServerError, "invalid /etc/fstab: %v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if _, err = shell.Execf(`df -h | grep '%s'`, req.Path); err != nil {
_, _ = shell.Execf(`sed -i 's@^s3fs#%s\s%s.*$@@g' /etc/fstab`, req.Bucket, req.Path)
service.Error(w, http.StatusInternalServerError, "mount failed: %v", err)
return
}
s3fsList = append(s3fsList, Mount{
ID: id,
Path: req.Path,
Bucket: req.Bucket,
Url: req.URL,
})
encoded, err := json.Marshal(s3fsList)
if err != nil {
service.Error(w, http.StatusInternalServerError, "failed to add s3fs mount")
return
}
if err = s.settingRepo.Set("s3fs", string(encoded)); err != nil {
service.Error(w, http.StatusInternalServerError, "failed to add s3fs mount")
service.Error(w, http.StatusInternalServerError, s.t.Get("mount failed: %v", err))
return
}
@@ -148,43 +120,38 @@ func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
return
}
var s3fsList []Mount
list, err := s.settingRepo.Get("s3fs", "[]")
list, err := s.mounts()
if err != nil {
service.Error(w, http.StatusInternalServerError, "failed to get s3fs list")
return
}
if err = json.Unmarshal([]byte(list), &s3fsList); err != nil {
service.Error(w, http.StatusInternalServerError, "failed to get s3fs list")
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get s3fs list: %v", err))
return
}
var mount Mount
for _, item := range s3fsList {
for _, item := range list {
if item.ID == req.ID {
mount = item
break
}
}
if mount.ID == 0 {
service.Error(w, http.StatusUnprocessableEntity, "mount not found")
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount not found"))
return
}
if _, err = shell.Execf(`fusermount -uz '%s'`, mount.Path); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if _, err = shell.Execf(`umount -lfq '%s'`, mount.Path); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
_, _ = shell.Execf(`fusermount -uz '%s'`, mount.Path)
_, err2 := shell.Execf(`umount -lf '%s'`, mount.Path)
// 卸载之后再检查下是否还有挂载
if _, err = shell.Execf(`df -h | grep '%s'`, mount.Path); err == nil {
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("failed to unmount: %v", err2))
return
}
if _, err = shell.Execf(`sed -i 's@^s3fs#%s\s%s.*$@@g' /etc/fstab`, mount.Bucket, mount.Path); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if _, err = shell.Execf("mount -a"); err != nil {
service.Error(w, http.StatusInternalServerError, "invalid /etc/fstab: %v", err)
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = io.Remove("/etc/passwd-s3fs-" + cast.ToString(mount.ID)); err != nil {
@@ -192,21 +159,57 @@ func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
return
}
var newS3fsList []Mount
for _, item := range s3fsList {
if item.ID != mount.ID {
newS3fsList = append(newS3fsList, item)
}
}
encoded, err := json.Marshal(newS3fsList)
if err != nil {
service.Error(w, http.StatusInternalServerError, "failed to delete s3fs mount")
return
}
if err = s.settingRepo.Set("s3fs", string(encoded)); err != nil {
service.Error(w, http.StatusInternalServerError, "failed to delete s3fs mount")
return
}
service.Success(w, nil)
}
func (s *App) mounts() ([]Mount, error) {
re := regexp.MustCompile(`^s3fs#(.*?)\s+(.*?)\s+fuse.*?url=(.*?),passwd_file=/etc/passwd-s3fs-(.*?)\s+`)
fstab, err := os.ReadFile("/etc/fstab")
if err != nil {
return nil, err
}
lines := strings.Split(string(fstab), "\n")
var mounts []Mount
ids, err := shell.Exec("find /etc -maxdepth 1 -name 'passwd-s3fs-*'")
if err != nil {
return nil, err
}
for _, id := range strings.Split(ids, "\n") {
if id == "" {
continue
}
id = strings.TrimPrefix(id, "/etc/passwd-s3fs-")
id = strings.TrimSuffix(id, "\n")
id = strings.TrimSpace(id)
if id == "" {
continue
}
mount := Mount{
ID: cast.ToInt64(id),
}
for _, line := range lines {
if line == "" {
continue
}
if strings.Contains(line, id) {
matches := re.FindStringSubmatch(line)
if len(matches) == 5 {
mount.Bucket = matches[1]
mount.Path = matches[2]
mount.URL = matches[3]
break
}
}
}
if mount.ID == 0 || mount.Path == "" || mount.Bucket == "" || mount.URL == "" {
continue
}
mounts = append(mounts, mount)
}
return mounts, nil
}
+1 -1
View File
@@ -4,5 +4,5 @@ type Mount struct {
ID int64 `json:"id"`
Path string `json:"path"`
Bucket string `json:"bucket"`
Url string `json:"url"`
URL string `json:"url"`
}
+21 -18
View File
@@ -6,6 +6,7 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-rat/chix"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/internal/service"
@@ -16,10 +17,11 @@ import (
)
type App struct {
t *gotext.Locale
name string
}
func NewApp() *App {
func NewApp(t *gotext.Locale) *App {
var name string
if os.IsRHEL() {
name = "supervisord"
@@ -28,13 +30,14 @@ func NewApp() *App {
}
return &App{
t: t,
name: name,
}
}
func (s *App) Route(r chi.Router) {
r.Get("/service", s.Service)
r.Post("/clearLog", s.ClearLog)
r.Post("/clear_log", s.ClearLog)
r.Get("/config", s.GetConfig)
r.Post("/config", s.UpdateConfig)
r.Get("/processes", s.Processes)
@@ -42,7 +45,7 @@ func (s *App) Route(r chi.Router) {
r.Post("/processes/{process}/stop", s.StopProcess)
r.Post("/processes/{process}/restart", s.RestartProcess)
r.Get("/processes/{process}/log", s.ProcessLog)
r.Post("/processes/{process}/clearLog", s.ClearProcessLog)
r.Post("/processes/{process}/clear_log", s.ClearProcessLog)
r.Get("/processes/{process}", s.ProcessConfig)
r.Post("/processes/{process}", s.UpdateProcessConfig)
r.Delete("/processes/{process}", s.DeleteProcess)
@@ -56,7 +59,7 @@ func (s *App) Service(w http.ResponseWriter, r *http.Request) {
// ClearLog 清空日志
func (s *App) ClearLog(w http.ResponseWriter, r *http.Request) {
if _, err := shell.Execf(`echo "" > /var/log/supervisor/supervisord.log`); err != nil {
if _, err := shell.Execf(`cat /dev/null > /var/log/supervisor/supervisord.log`); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -102,7 +105,7 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
}
if err = systemctl.Restart(s.name); err != nil {
service.Error(w, http.StatusInternalServerError, "重启 %s 服务失败: %v", s.name, err)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to restart %s: %v", s.name, err))
return
}
@@ -156,8 +159,8 @@ func (s *App) StartProcess(w http.ResponseWriter, r *http.Request) {
return
}
if _, err = shell.Execf(`supervisorctl start %s`, req.Process); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
if out, err := shell.Execf(`supervisorctl start %s`, req.Process); err != nil {
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
return
}
@@ -172,8 +175,8 @@ func (s *App) StopProcess(w http.ResponseWriter, r *http.Request) {
return
}
if _, err = shell.Execf(`supervisorctl stop %s`, req.Process); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
if out, err := shell.Execf(`supervisorctl stop %s`, req.Process); err != nil {
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
return
}
@@ -188,8 +191,8 @@ func (s *App) RestartProcess(w http.ResponseWriter, r *http.Request) {
return
}
if _, err = shell.Execf(`supervisorctl restart %s`, req.Process); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
if out, err := shell.Execf(`supervisorctl restart %s`, req.Process); err != nil {
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
return
}
@@ -212,7 +215,7 @@ func (s *App) ProcessLog(w http.ResponseWriter, r *http.Request) {
}
if err != nil {
service.Error(w, http.StatusInternalServerError, "无法从进程 %s 的配置文件中获取日志路径", req.Process)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
return
}
@@ -235,11 +238,11 @@ func (s *App) ClearProcessLog(w http.ResponseWriter, r *http.Request) {
}
if err != nil {
service.Error(w, http.StatusInternalServerError, "无法从进程 %s 的配置文件中获取日志路径", req.Process)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
return
}
if _, err = shell.Execf(`echo "" > '%s'`, logPath); err != nil {
if _, err = shell.Execf(`cat /dev/null > '%s'`, logPath); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
@@ -343,8 +346,8 @@ func (s *App) DeleteProcess(w http.ResponseWriter, r *http.Request) {
return
}
if _, err = shell.Execf(`supervisorctl stop '%s'`, req.Process); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
if out, err := shell.Execf(`supervisorctl stop '%s'`, req.Process); err != nil {
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
return
}
@@ -352,7 +355,7 @@ func (s *App) DeleteProcess(w http.ResponseWriter, r *http.Request) {
if os.IsRHEL() {
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
if err != nil {
service.Error(w, http.StatusInternalServerError, "无法从进程 %s 的配置文件中获取日志路径", req.Process)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
return
}
if err = io.Remove(`/etc/supervisord.d/` + req.Process + `.conf`); err != nil {
@@ -362,7 +365,7 @@ func (s *App) DeleteProcess(w http.ResponseWriter, r *http.Request) {
} else {
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
if err != nil {
service.Error(w, http.StatusInternalServerError, "无法从进程 %s 的配置文件中获取日志路径", req.Process)
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
return
}
if err = io.Remove(`/etc/supervisor/conf.d/` + req.Process + `.conf`); err != nil {
+5 -5
View File
@@ -8,11 +8,11 @@ import (
type App struct {
ID uint `gorm:"primaryKey" json:"id"`
Slug string `gorm:"not null;unique" json:"slug"`
Channel string `gorm:"not null" json:"channel"`
Version string `gorm:"not null" json:"version"`
Show bool `gorm:"not null" json:"show"`
ShowOrder int `gorm:"not null" json:"show_order"`
Slug string `gorm:"not null;default:'';unique" json:"slug"`
Channel string `gorm:"not null;default:''" json:"channel"`
Version string `gorm:"not null;default:''" json:"version"`
Show bool `gorm:"not null;default:false" json:"show"`
ShowOrder int `gorm:"not null;default:0" json:"show_order"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+1 -1
View File
@@ -11,7 +11,7 @@ const (
type Cache struct {
Key CacheKey `gorm:"primaryKey" json:"key"`
Value string `gorm:"not null" json:"value"`
Value string `gorm:"not null;default:''" json:"value"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+10 -9
View File
@@ -10,15 +10,16 @@ import (
type Cert struct {
ID uint `gorm:"primaryKey" json:"id"`
AccountID uint `gorm:"not null" json:"account_id"` // 关联的 ACME 账户 ID
WebsiteID uint `gorm:"not null" json:"website_id"` // 关联的网站 ID
DNSID uint `gorm:"not null" json:"dns_id"` // 关联的 DNS ID
Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096)
Domains []string `gorm:"not null;serializer:json" json:"domains"`
AutoRenew bool `gorm:"not null" json:"auto_renew"` // 自动续签
CertURL string `gorm:"not null" json:"cert_url"` // 证书 URL (续签时使用)
Cert string `gorm:"not null" json:"cert"` // 证书内容
Key string `gorm:"not null" json:"key"` // 私钥内容
AccountID uint `gorm:"not null;default:0" json:"account_id"` // 关联的 ACME 账户 ID
WebsiteID uint `gorm:"not null;default:0" json:"website_id"` // 关联的网站 ID
DNSID uint `gorm:"not null;default:0" json:"dns_id"` // 关联的 DNS ID
Type string `gorm:"not null;default:''" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096)
Domains []string `gorm:"not null;default:'[]';serializer:json" json:"domains"`
AutoRenew bool `gorm:"not null;default:false" json:"auto_renew"` // 自动续签
CertURL string `gorm:"not null;default:''" json:"cert_url"` // 证书 URL (续签时使用)
Cert string `gorm:"not null;default:''" json:"cert"` // 证书内容
Key string `gorm:"not null;default:''" json:"key"` // 私钥内容
Script string `gorm:"not null;default:''" json:"script"` // 部署脚本
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
+6 -6
View File
@@ -8,12 +8,12 @@ import (
type CertAccount struct {
ID uint `gorm:"primaryKey" json:"id"`
Email string `gorm:"not null" json:"email"`
CA string `gorm:"not null" json:"ca"` // CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass)
Kid string `gorm:"not null" json:"kid"`
HmacEncoded string `gorm:"not null" json:"hmac_encoded"`
PrivateKey string `gorm:"not null" json:"private_key"`
KeyType string `gorm:"not null" json:"key_type"` // 密钥类型 (P256, P384, 2048, 3072, 4096)
Email string `gorm:"not null;default:''" json:"email"`
CA string `gorm:"not null;default:'letsencrypt'" json:"ca"` // CA 提供商 (letsencrypt, zerossl, sslcom, google, buypass)
Kid string `gorm:"not null;default:''" json:"kid"`
HmacEncoded string `gorm:"not null;default:''" json:"hmac_encoded"`
PrivateKey string `gorm:"not null;default:''" json:"private_key"`
KeyType string `gorm:"not null;default:'P256'" json:"key_type"` // 密钥类型 (P256, P384, 2048, 3072, 4096)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
+2 -2
View File
@@ -9,8 +9,8 @@ import (
type CertDNS struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"` // 备注名称
Type string `gorm:"not null" json:"type"` // DNS 提供商 (tencent, aliyun, cloudflare)
Name string `gorm:"not null;default:''" json:"name"` // 备注名称
Type acme.DnsType `gorm:"not null;default:'aliyun'" json:"type"` // DNS 提供商
Data acme.DNSParam `gorm:"not null;serializer:json" json:"dns_param"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
+13
View File
@@ -0,0 +1,13 @@
package biz
import "github.com/tnb-labs/panel/pkg/types"
type ContainerComposeRepo interface {
List() ([]types.ContainerCompose, error)
Get(name string) (string, []types.KV, error)
Create(name, compose string, envs []types.KV) error
Update(name, compose string, envs []types.KV) error
Up(name string, force bool) error
Down(name string) error
Remove(name string) error
}
+6 -6
View File
@@ -8,12 +8,12 @@ import (
type Cron struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null;unique" json:"name"`
Status bool `gorm:"not null" json:"status"`
Type string `gorm:"not null" json:"type"`
Time string `gorm:"not null" json:"time"`
Shell string `gorm:"not null" json:"shell"`
Log string `gorm:"not null" json:"log"`
Name string `gorm:"not null;default:'';unique" json:"name"`
Status bool `gorm:"not null;default:false" json:"status"`
Type string `gorm:"not null;default:''" json:"type"`
Time string `gorm:"not null;default:''" json:"time"`
Shell string `gorm:"not null;default:''" json:"shell"`
Log string `gorm:"not null;default:''" json:"log"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+7 -7
View File
@@ -19,14 +19,14 @@ const (
type DatabaseServer struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null;unique" json:"name"`
Type DatabaseType `gorm:"not null" json:"type"`
Host string `gorm:"not null" json:"host"`
Port uint `gorm:"not null" json:"port"`
Username string `gorm:"not null" json:"username"`
Password string `gorm:"not null" json:"password"`
Name string `gorm:"not null;default:'';unique" json:"name"`
Type DatabaseType `gorm:"not null;default:''" json:"type"`
Host string `gorm:"not null;default:''" json:"host"`
Port uint `gorm:"not null;default:0" json:"port"`
Username string `gorm:"not null;default:''" json:"username"`
Password string `gorm:"not null;default:''" json:"password"`
Status DatabaseServerStatus `gorm:"-:all" json:"status"`
Remark string `gorm:"not null" json:"remark"`
Remark string `gorm:"not null;default:''" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+7 -7
View File
@@ -19,13 +19,13 @@ const (
type DatabaseUser struct {
ID uint `gorm:"primaryKey" json:"id"`
ServerID uint `gorm:"not null" json:"server_id"`
Username string `gorm:"not null" json:"username"`
Password string `gorm:"not null" json:"password"`
Host string `gorm:"not null" json:"host"` // 仅 mysql
Status DatabaseUserStatus `gorm:"-:all" json:"status"` // 仅显示
Privileges []string `gorm:"-:all" json:"privileges"` // 仅显示
Remark string `gorm:"not null" json:"remark"`
ServerID uint `gorm:"not null;default:0" json:"server_id"`
Username string `gorm:"not null;default:''" json:"username"`
Password string `gorm:"not null;default:''" json:"password"`
Host string `gorm:"not null;default:''" json:"host"` // 仅 mysql
Status DatabaseUserStatus `gorm:"-:all" json:"status"` // 仅显示
Privileges []string `gorm:"-:all" json:"privileges"` // 仅显示
Remark string `gorm:"not null;default:''" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
+1 -1
View File
@@ -9,7 +9,7 @@ import (
type Monitor struct {
ID uint `gorm:"primaryKey" json:"id"`
Info types.CurrentInfo `gorm:"not null;serializer:json" json:"info"`
Info types.CurrentInfo `gorm:"not null;default:'{}';serializer:json" json:"info"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+9 -5
View File
@@ -1,7 +1,6 @@
package biz
import (
"context"
"time"
"github.com/tnb-labs/panel/internal/http/request"
@@ -12,6 +11,7 @@ type SettingKey string
const (
SettingKeyName SettingKey = "name"
SettingKeyVersion SettingKey = "version"
SettingKeyChannel SettingKey = "channel"
SettingKeyMonitor SettingKey = "monitor"
SettingKeyMonitorDays SettingKey = "monitor_days"
SettingKeyBackupPath SettingKey = "backup_path"
@@ -23,8 +23,8 @@ const (
type Setting struct {
ID uint `gorm:"primaryKey" json:"id"`
Key SettingKey `gorm:"not null;unique" json:"key"`
Value string `gorm:"not null" json:"value"`
Key SettingKey `gorm:"not null;default:'';unique" json:"key"`
Value string `gorm:"not null;default:''" json:"value"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -32,8 +32,12 @@ type Setting struct {
type SettingRepo interface {
Get(key SettingKey, defaultValue ...string) (string, error)
GetBool(key SettingKey, defaultValue ...bool) (bool, error)
GetInt(key SettingKey, defaultValue ...int) (int, error)
GetSlice(key SettingKey, defaultValue ...[]string) ([]string, error)
Set(key SettingKey, value string) error
SetSlice(key SettingKey, value []string) error
Delete(key SettingKey) error
GetPanelSetting(ctx context.Context) (*request.PanelSetting, error)
UpdatePanelSetting(ctx context.Context, setting *request.PanelSetting) (bool, error)
GetPanel() (*request.SettingPanel, error)
UpdatePanel(req *request.SettingPanel) (bool, error)
UpdateCert(req *request.SettingCert) error
}
+5 -6
View File
@@ -13,11 +13,11 @@ import (
type SSH struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null" json:"name"`
Host string `gorm:"not null" json:"host"`
Port uint `gorm:"not null" json:"port"`
Config ssh.ClientConfig `gorm:"not null;serializer:json" json:"config"`
Remark string `gorm:"not null" json:"remark"`
Name string `gorm:"not null;default:''" json:"name"`
Host string `gorm:"not null;default:''" json:"host"`
Port uint `gorm:"not null;default:0" json:"port"`
Config ssh.ClientConfig `gorm:"not null;default:'{}';serializer:json" json:"config"`
Remark string `gorm:"not null;default:''" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -38,7 +38,6 @@ func (r *SSH) BeforeSave(tx *gorm.DB) error {
}
return nil
}
func (r *SSH) AfterFind(tx *gorm.DB) error {
+3 -3
View File
@@ -13,10 +13,10 @@ const (
type Task struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null;index" json:"name"`
Name string `gorm:"not null;default:'';index" json:"name"`
Status TaskStatus `gorm:"not null;default:'waiting'" json:"status"`
Shell string `gorm:"not null" json:"-"`
Log string `gorm:"not null" json:"log"`
Shell string `gorm:"not null;default:''" json:"-"`
Log string `gorm:"not null;default:''" json:"log"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
+16 -6
View File
@@ -1,6 +1,7 @@
package biz
import (
"image"
"time"
"gorm.io/gorm"
@@ -8,17 +9,26 @@ import (
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"not null;unique" json:"username"`
Password string `gorm:"not null" json:"password"`
Email string `gorm:"not null" json:"email"`
Username string `gorm:"not null;default:'';unique" json:"username"`
Password string `gorm:"not null;default:''" json:"password"`
Email string `gorm:"not null;default:''" json:"email"`
TwoFA string `gorm:"not null;default:''" json:"two_fa"` // 2FA secret,为空表示未开启
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"`
Tokens []*UserToken `gorm:"foreignKey:UserID" json:"-"`
}
type UserRepo interface {
Create(username, password string) (*User, error)
CheckPassword(username, password string) (*User, error)
List(page, limit uint) ([]*User, int64, error)
Get(id uint) (*User, error)
Save(user *User) error
Create(username, password, email string) (*User, error)
UpdatePassword(id uint, password string) error
UpdateEmail(id uint, email string) error
Delete(id uint) error
CheckPassword(username, password string) (*User, error)
IsTwoFA(username string) (bool, error)
GenerateTwoFA(id uint) (image.Image, string, string, error)
UpdateTwoFA(id uint, code, secret string) error
}
+58
View File
@@ -0,0 +1,58 @@
package biz
import (
"net/http"
"time"
"github.com/go-rat/utils/crypt"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/app"
)
type UserToken struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index" json:"user_id"`
Token string `gorm:"not null;default:'';unique" json:"-"`
IPs []string `gorm:"not null;default:'[]';serializer:json" json:"ips"`
ExpiredAt time.Time `json:"expired_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (r *UserToken) BeforeSave(tx *gorm.DB) error {
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
if err != nil {
return err
}
r.Token, err = crypter.Encrypt([]byte(r.Token))
if err != nil {
return err
}
return nil
}
func (r *UserToken) AfterFind(tx *gorm.DB) error {
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
if err != nil {
return err
}
token, err := crypter.Decrypt(r.Token)
if err == nil {
r.Token = string(token)
}
return nil
}
type UserTokenRepo interface {
List(userID, page, limit uint) ([]*UserToken, int64, error)
Create(userID uint, ips []string, expired time.Time) (*UserToken, error)
Get(id uint) (*UserToken, error)
Delete(id uint) error
Update(id uint, ips []string, expired time.Time) (*UserToken, error)
ValidateReq(req *http.Request) (uint, error)
}
+6 -4
View File
@@ -10,11 +10,12 @@ import (
type Website struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null;unique" json:"name"`
Name string `gorm:"not null;default:'';unique" json:"name"`
Type string `gorm:"not null;default:'php'" json:"type"`
Status bool `gorm:"not null;default:true" json:"status"`
Path string `gorm:"not null" json:"path"`
Https bool `gorm:"not null" json:"https"`
Remark string `gorm:"not null" json:"remark"`
Path string `gorm:"not null;default:''" json:"path"`
Https bool `gorm:"not null;default:false" json:"https"`
Remark string `gorm:"not null;default:''" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -35,5 +36,6 @@ type WebsiteRepo interface {
UpdateRemark(id uint, remark string) error
ResetConfig(id uint) error
UpdateStatus(id uint, status bool) error
UpdateCert(req *request.WebsiteUpdateCert) error
ObtainCert(ctx context.Context, id uint) error
}
+5 -5
View File
@@ -1,12 +1,13 @@
package bootstrap
import (
"github.com/tnb-labs/panel/internal/apps/benchmark"
"github.com/tnb-labs/panel/internal/apps/codeserver"
"github.com/tnb-labs/panel/internal/apps/docker"
"github.com/tnb-labs/panel/internal/apps/fail2ban"
"github.com/tnb-labs/panel/internal/apps/frp"
"github.com/tnb-labs/panel/internal/apps/gitea"
"github.com/tnb-labs/panel/internal/apps/memcached"
"github.com/tnb-labs/panel/internal/apps/minio"
"github.com/tnb-labs/panel/internal/apps/mysql"
"github.com/tnb-labs/panel/internal/apps/nginx"
"github.com/tnb-labs/panel/internal/apps/php74"
@@ -23,17 +24,17 @@ import (
"github.com/tnb-labs/panel/internal/apps/rsync"
"github.com/tnb-labs/panel/internal/apps/s3fs"
"github.com/tnb-labs/panel/internal/apps/supervisor"
"github.com/tnb-labs/panel/internal/apps/toolbox"
"github.com/tnb-labs/panel/pkg/apploader"
)
func NewLoader(
benchmark *benchmark.App,
codeserver *codeserver.App,
docker *docker.App,
fail2ban *fail2ban.App,
frp *frp.App,
gitea *gitea.App,
memcached *memcached.App,
minio *minio.App,
mysql *mysql.App,
nginx *nginx.App,
php74 *php74.App,
@@ -50,9 +51,8 @@ func NewLoader(
rsync *rsync.App,
s3fs *s3fs.App,
supervisor *supervisor.App,
toolbox *toolbox.App,
) *apploader.Loader {
loader := new(apploader.Loader)
loader.Add(benchmark, docker, fail2ban, frp, gitea, memcached, mysql, nginx, php74, php80, php81, php82, php83, php84, phpmyadmin, podman, postgresql, pureftpd, redis, rsync, s3fs, supervisor, toolbox)
loader.Add(codeserver, docker, fail2ban, frp, gitea, memcached, minio, mysql, nginx, php74, php80, php81, php82, php83, php84, phpmyadmin, podman, postgresql, pureftpd, redis, rsync, s3fs, supervisor)
return loader
}
+1 -1
View File
@@ -3,4 +3,4 @@ package bootstrap
import "github.com/google/wire"
// ProviderSet is bootstrap providers.
var ProviderSet = wire.NewSet(NewConf, NewLog, NewCli, NewValidator, NewRouter, NewHttp, NewDB, NewMigrate, NewLoader, NewSession, NewCron, NewQueue)
var ProviderSet = wire.NewSet(NewConf, NewT, NewLog, NewCli, NewValidator, NewRouter, NewHttp, NewDB, NewMigrate, NewLoader, NewSession, NewCron, NewQueue)
+24 -23
View File
@@ -3,39 +3,40 @@ package bootstrap
import (
"strings"
"github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v3"
"github.com/tnb-labs/panel/internal/app"
"github.com/tnb-labs/panel/internal/route"
)
func NewCli(cmd *route.Cli) *cli.Command {
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "NAME", "名称")
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "USAGE", "用法")
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "VERSION", "版本")
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "DESCRIPTION", "描述")
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "AUTHOR", "作者")
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "COMMANDS", "命令")
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "GLOBAL OPTIONS", "全局选项")
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "COPYRIGHT", "版权")
cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "NAME", "名称")
cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "USAGE", "用法")
cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "CATEGORY", "分类")
cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "DESCRIPTION", "描述")
cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "OPTIONS", "选项")
cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "NAME", "名称")
cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "USAGE", "用法")
cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "DESCRIPTION", "描述")
cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "COMMANDS", "命令")
cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "OPTIONS", "选项")
func NewCli(t *gotext.Locale, cmd *route.Cli) *cli.Command {
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "NAME", t.Get("NAME"))
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "USAGE", t.Get("USAGE"))
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "VERSION", t.Get("VERSION"))
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "DESCRIPTION", t.Get("DESCRIPTION"))
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "AUTHOR", t.Get("AUTHOR"))
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "COMMANDS", t.Get("COMMANDS"))
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "GLOBAL OPTIONS", t.Get("GLOBAL OPTIONS"))
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "COPYRIGHT", t.Get("COPYRIGHT"))
cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "NAME", t.Get("NAME"))
cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "USAGE", t.Get("USAGE"))
cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "CATEGORY", t.Get("CATEGORY"))
cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "DESCRIPTION", t.Get("DESCRIPTION"))
cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "OPTIONS", t.Get("OPTIONS"))
cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "NAME", t.Get("NAME"))
cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "USAGE", t.Get("USAGE"))
cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "DESCRIPTION", t.Get("USAGE"))
cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "COMMANDS", t.Get("COMMANDS"))
cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "OPTIONS", t.Get("OPTIONS"))
cli.RootCommandHelpTemplate += "\n官网https://panel.haozi.net"
cli.RootCommandHelpTemplate += "\n论坛https://bbs.haozi.net"
cli.RootCommandHelpTemplate += "\nQ群:12370907\n"
cli.RootCommandHelpTemplate += "\n" + t.Get("Websitehttps://panel.haozi.net")
cli.RootCommandHelpTemplate += "\n" + t.Get("Forumhttps://bbs.haozi.net")
cli.RootCommandHelpTemplate += "\n" + t.Get("QQ Group12370907") + "\n"
return &cli.Command{
Name: "panel-cli",
Usage: "耗子面板命令行工具",
Usage: t.Get("Rat Panel CLI Tool"),
Version: app.Version,
Commands: cmd.Commands(),
}
+1 -1
View File
@@ -30,7 +30,7 @@ func NewConf() (*koanf.Koanf, error) {
func initGlobal(conf *koanf.Koanf) {
app.Key = conf.MustString("app.key")
if len(app.Key) != 32 {
log.Fatalf("app key must be 32 characters")
log.Fatalf("panel app key must be 32 characters")
}
app.Root = conf.MustString("app.root")
+3 -2
View File
@@ -4,9 +4,10 @@ import (
"log/slog"
"path/filepath"
"github.com/glebarez/sqlite"
"github.com/go-gormigrate/gormigrate/v2"
"github.com/knadh/koanf/v2"
_ "github.com/ncruces/go-sqlite3/embed"
"github.com/ncruces/go-sqlite3/gormlite"
sloggorm "github.com/orandin/slog-gorm"
"gorm.io/gorm"
@@ -16,7 +17,7 @@ import (
func NewDB(conf *koanf.Koanf, log *slog.Logger) (*gorm.DB, error) {
// You can use any other database, like MySQL or PostgreSQL.
return gorm.Open(sqlite.Open(filepath.Join(app.Root, "panel/storage/app.db")), &gorm.Config{
return gorm.Open(gormlite.Open(filepath.Join(app.Root, "panel/storage/app.db")), &gorm.Config{
Logger: sloggorm.New(sloggorm.WithHandler(log.Handler())),
SkipDefaultTransaction: true,
DisableForeignKeyConstraintWhenMigrating: true,
+3 -2
View File
@@ -8,16 +8,17 @@ import (
"github.com/bddjr/hlfhr"
"github.com/go-chi/chi/v5"
"github.com/knadh/koanf/v2"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/http/middleware"
"github.com/tnb-labs/panel/internal/route"
)
func NewRouter(middlewares *middleware.Middlewares, http *route.Http, ws *route.Ws) (*chi.Mux, error) {
func NewRouter(t *gotext.Locale, middlewares *middleware.Middlewares, http *route.Http, ws *route.Ws) (*chi.Mux, error) {
r := chi.NewRouter()
// add middleware
r.Use(middlewares.Globals(r)...)
r.Use(middlewares.Globals(t, r)...)
// add http route
http.Register(r)
// add ws route
+6 -1
View File
@@ -9,9 +9,14 @@ import (
func NewSession(conf *koanf.Koanf, db *gorm.DB) (*sessions.Manager, error) {
// initialize session manager
lifetime := conf.Int("session.lifetime")
// TODO: will remove this fallback in v3
if lifetime == 0 {
lifetime = 120
}
manager, err := sessions.NewManager(&sessions.ManagerOptions{
Key: conf.MustString("app.key"),
Lifetime: 120,
Lifetime: lifetime,
GcInterval: 5,
DisableDefaultDriver: true,
})
+16
View File
@@ -0,0 +1,16 @@
package bootstrap
import (
"github.com/knadh/koanf/v2"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/pkg/embed"
)
func NewT(conf *koanf.Koanf) (*gotext.Locale, error) {
locale := conf.String("app.locale")
l := gotext.NewLocaleFSWithPath(locale, embed.LocalesFS, "locales")
l.AddDomain("backend")
return l, nil
}
+11 -3
View File
@@ -2,19 +2,27 @@ 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"
"github.com/knadh/koanf/v2"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/http/rule"
)
// NewValidator just for register global rules
func NewValidator(db *gorm.DB) *validate.Validation {
zhcn.RegisterGlobal()
func NewValidator(conf *koanf.Koanf, db *gorm.DB) *validate.Validation {
if conf.String("app.locale") == "zh_CN" {
zhcn.RegisterGlobal()
} else if conf.String("app.locale") == "zh_TW" {
zhtw.RegisterGlobal()
} else if conf.String("app.locale") == "ru_RU" {
ruru.RegisterGlobal()
}
validate.Config(func(opt *validate.GlobalOption) {
opt.StopOnError = false
opt.SkipOnEmpty = true
opt.FieldTag = "form"
})
// register global rules
+18 -15
View File
@@ -9,6 +9,7 @@ import (
"github.com/expr-lang/expr"
"github.com/go-rat/utils/collect"
"github.com/hashicorp/go-version"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"gorm.io/gorm"
@@ -19,13 +20,15 @@ import (
)
type appRepo struct {
t *gotext.Locale
db *gorm.DB
cache biz.CacheRepo
task biz.TaskRepo
}
func NewAppRepo(db *gorm.DB, cache biz.CacheRepo, task biz.TaskRepo) biz.AppRepo {
func NewAppRepo(t *gotext.Locale, db *gorm.DB, cache biz.CacheRepo, task biz.TaskRepo) biz.AppRepo {
return &appRepo{
t: t,
db: db,
cache: cache,
task: task,
@@ -50,7 +53,7 @@ func (r *appRepo) Get(slug string) (*api.App, error) {
return item, nil
}
}
return nil, errors.New("应用不存在")
return nil, errors.New(r.t.Get("app %s not found", slug))
}
func (r *appRepo) UpdateExist(slug string) bool {
@@ -153,7 +156,7 @@ func (r *appRepo) Install(channel, slug string) error {
}
if installed, _ := r.IsInstalled(slug); installed {
return errors.New("应用已安装")
return errors.New(r.t.Get("app %s already installed", slug))
}
shellUrl, shellChannel, shellVersion := "", "", ""
@@ -164,7 +167,7 @@ func (r *appRepo) Install(channel, slug string) error {
}
if ch.Slug == channel {
if vs.GreaterThan(panel) {
return fmt.Errorf("应用 %s 需要面板版本 %s,当前版本 %s", item.Name, ch.Panel, app.Version)
return errors.New(r.t.Get("app %s requires panel version %s, current version %s", item.Name, ch.Panel, app.Version))
}
shellUrl = ch.Install
shellChannel = ch.Slug
@@ -173,7 +176,7 @@ func (r *appRepo) Install(channel, slug string) error {
}
}
if shellUrl == "" {
return fmt.Errorf("应用 %s 不支持当前面板版本", item.Name)
return errors.New(r.t.Get("app %s not support current panel version", item.Name))
}
if err = r.preCheck(item); err != nil {
@@ -185,7 +188,7 @@ func (r *appRepo) Install(channel, slug string) error {
}
task := new(biz.Task)
task.Name = "安装应用 " + item.Name
task.Name = r.t.Get("Install app %s", item.Name)
task.Status = biz.TaskStatusWaiting
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
task.Log = "/tmp/" + item.Slug + ".log"
@@ -204,7 +207,7 @@ func (r *appRepo) UnInstall(slug string) error {
}
if installed, _ := r.IsInstalled(slug); !installed {
return errors.New("应用未安装")
return errors.New(r.t.Get("app %s not installed", item.Name))
}
installed, err := r.GetInstalled(slug)
if err != nil {
@@ -219,7 +222,7 @@ func (r *appRepo) UnInstall(slug string) error {
}
if ch.Slug == installed.Channel {
if vs.GreaterThan(panel) {
return fmt.Errorf("应用 %s 需要面板版本 %s,当前版本 %s", item.Name, ch.Panel, app.Version)
return errors.New(r.t.Get("app %s requires panel version %s, current version %s", item.Name, ch.Panel, app.Version))
}
shellUrl = ch.Uninstall
shellChannel = ch.Slug
@@ -228,7 +231,7 @@ func (r *appRepo) UnInstall(slug string) error {
}
}
if shellUrl == "" {
return fmt.Errorf("无法获取应用 %s 的卸载脚本", item.Name)
return errors.New(r.t.Get("failed to get uninstall script for app %s", item.Name))
}
if err = r.preCheck(item); err != nil {
@@ -240,7 +243,7 @@ func (r *appRepo) UnInstall(slug string) error {
}
task := new(biz.Task)
task.Name = "卸载应用 " + item.Name
task.Name = r.t.Get("Uninstall app %s", item.Name)
task.Status = biz.TaskStatusWaiting
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
task.Log = "/tmp/" + item.Slug + ".log"
@@ -259,7 +262,7 @@ func (r *appRepo) Update(slug string) error {
}
if installed, _ := r.IsInstalled(slug); !installed {
return errors.New("应用未安装")
return errors.New(r.t.Get("app %s not installed", item.Name))
}
installed, err := r.GetInstalled(slug)
if err != nil {
@@ -274,7 +277,7 @@ func (r *appRepo) Update(slug string) error {
}
if ch.Slug == installed.Channel {
if vs.GreaterThan(panel) {
return fmt.Errorf("应用 %s 需要面板版本 %s,当前版本 %s", item.Name, ch.Panel, app.Version)
return errors.New(r.t.Get("app %s requires panel version %s, current version %s", item.Name, ch.Panel, app.Version))
}
shellUrl = ch.Update
shellChannel = ch.Slug
@@ -283,7 +286,7 @@ func (r *appRepo) Update(slug string) error {
}
}
if shellUrl == "" {
return fmt.Errorf("应用 %s 不支持当前面板版本", item.Name)
return errors.New(r.t.Get("app %s not support current panel version", item.Name))
}
if err = r.preCheck(item); err != nil {
@@ -295,7 +298,7 @@ func (r *appRepo) Update(slug string) error {
}
task := new(biz.Task)
task.Name = "更新应用 " + item.Name
task.Name = r.t.Get("Update app %s", item.Name)
task.Status = biz.TaskStatusWaiting
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
task.Log = "/tmp/" + item.Slug + ".log"
@@ -341,7 +344,7 @@ func (r *appRepo) preCheck(app *api.App) error {
result := cast.ToString(output)
if result != "ok" {
return fmt.Errorf("应用 %s %s", app.Name, result)
return errors.New(r.t.Get("App %s %s", app.Name, result))
}
return nil
+115 -120
View File
@@ -9,6 +9,7 @@ import (
"strings"
"time"
"github.com/leonelquinteros/gotext"
"github.com/shirou/gopsutil/disk"
"gorm.io/gorm"
@@ -22,13 +23,15 @@ import (
)
type backupRepo struct {
t *gotext.Locale
db *gorm.DB
setting biz.SettingRepo
website biz.WebsiteRepo
}
func NewBackupRepo(db *gorm.DB, setting biz.SettingRepo, website biz.WebsiteRepo) biz.BackupRepo {
func NewBackupRepo(t *gotext.Locale, db *gorm.DB, setting biz.SettingRepo, website biz.WebsiteRepo) biz.BackupRepo {
return &backupRepo{
t: t,
db: db,
setting: setting,
website: website,
@@ -89,7 +92,7 @@ func (r *backupRepo) Create(typ biz.BackupType, target string, path ...string) e
}
return errors.New("未知备份类型")
return errors.New(r.t.Get("unknown backup type"))
}
// Delete 删除备份
@@ -125,7 +128,7 @@ func (r *backupRepo) Restore(typ biz.BackupType, backup, target string) error {
return r.restorePostgres(backup, target)
}
return errors.New("未知备份类型")
return errors.New(r.t.Get("unknown backup type"))
}
// CutoffLog 切割日志
@@ -133,7 +136,7 @@ func (r *backupRepo) Restore(typ biz.BackupType, backup, target string) error {
// target 待切割日志文件绝对路径
func (r *backupRepo) CutoffLog(path, target string) error {
if !io.Exists(target) {
return errors.New("日志文件不存在")
return errors.New(r.t.Get("log file %s not exists", target))
}
to := filepath.Join(path, fmt.Sprintf("%s_%s.zip", time.Now().Format("20060102150405"), filepath.Base(target)))
@@ -141,7 +144,12 @@ func (r *backupRepo) CutoffLog(path, target string) error {
return err
}
return io.Remove(target)
// 原文件不能直接删除,直接删的话仍会占用空间直到重启相关的应用
if _, err := shell.Execf("cat /dev/null > '%s'", target); err != nil {
return err
}
return nil
}
// ClearExpired 清理过期备份
@@ -184,14 +192,10 @@ func (r *backupRepo) ClearExpired(path, prefix string, save int) error {
for _, file := range toDelete {
filePath := filepath.Join(path, file.Name())
if app.IsCli {
fmt.Printf("|-清理过期文件:%s\n", filePath)
fmt.Println(r.t.Get("|-Cleaning expired file: %s", filePath))
}
if err = os.Remove(filePath); err != nil {
if app.IsCli {
fmt.Printf("|-清理失败:%v\n", err)
} else {
return fmt.Errorf("清理失败:%v", err)
}
return errors.New(r.t.Get("Cleanup failed: %v", err))
}
}
@@ -205,7 +209,7 @@ func (r *backupRepo) GetPath(typ biz.BackupType) (string, error) {
return "", err
}
if !slices.Contains([]biz.BackupType{biz.BackupTypePath, biz.BackupTypeWebsite, biz.BackupTypeMySQL, biz.BackupTypePostgres, biz.BackupTypeRedis, biz.BackupTypePanel}, typ) {
return "", errors.New("未知备份类型")
return "", errors.New(r.t.Get("unknown backup type"))
}
backupPath = filepath.Join(backupPath, string(typ))
@@ -236,8 +240,8 @@ func (r *backupRepo) createWebsite(to string, name string) error {
}
if app.IsCli {
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup))
fmt.Println(r.t.Get("|-Backup time: %s", time.Since(start).String()))
fmt.Println(r.t.Get("|-Backed up to file: %s", filepath.Base(backup)))
}
return nil
}
@@ -252,9 +256,11 @@ func (r *backupRepo) createMySQL(to string, name string) error {
if err != nil {
return err
}
defer mysql.Close()
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
if exist, _ := mysql.DatabaseExists(name); !exist {
return fmt.Errorf("数据库不存在:%s", name)
return errors.New(r.t.Get("database does not exist: %s", name))
}
size, err := mysql.DatabaseSize(name)
if err != nil {
@@ -284,8 +290,8 @@ func (r *backupRepo) createMySQL(to string, name string) error {
}
if app.IsCli {
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup+".zip"))
fmt.Println(r.t.Get("|-Backup time: %s", time.Since(start).String()))
fmt.Println(r.t.Get("|-Backed up to file: %s", filepath.Base(backup+".zip")))
}
return nil
}
@@ -296,9 +302,11 @@ func (r *backupRepo) createPostgres(to string, name string) error {
if err != nil {
return err
}
defer postgres.Close()
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
if exist, _ := postgres.DatabaseExist(name); !exist {
return fmt.Errorf("数据库不存在:%s", name)
return errors.New(r.t.Get("database does not exist: %s", name))
}
size, err := postgres.DatabaseSize(name)
if err != nil {
@@ -322,8 +330,8 @@ func (r *backupRepo) createPostgres(to string, name string) error {
}
if app.IsCli {
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup+".zip"))
fmt.Println(r.t.Get("|-Backup time: %s", time.Since(start).String()))
fmt.Println(r.t.Get("|-Backed up to file: %s", filepath.Base(backup+".zip")))
}
return nil
}
@@ -362,8 +370,8 @@ func (r *backupRepo) createPanel(to string) error {
}
if app.IsCli {
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup))
fmt.Println(r.t.Get("|-Backup time: %s", time.Since(start).String()))
fmt.Println(r.t.Get("|-Backed up to file: %s", filepath.Base(backup)))
}
return io.Remove(temp)
@@ -372,7 +380,7 @@ func (r *backupRepo) createPanel(to string) error {
// restoreWebsite 恢复网站备份
func (r *backupRepo) restoreWebsite(backup, target string) error {
if !io.Exists(backup) {
return errors.New("备份文件不存在")
return errors.New(r.t.Get("backup file %s not exists", backup))
}
website, err := r.website.GetByName(target)
@@ -399,7 +407,7 @@ func (r *backupRepo) restoreWebsite(backup, target string) error {
// restoreMySQL 恢复 MySQL 备份
func (r *backupRepo) restoreMySQL(backup, target string) error {
if !io.Exists(backup) {
return errors.New("备份文件不存在")
return errors.New(r.t.Get("backup file %s not exists", backup))
}
rootPassword, err := r.setting.Get(biz.SettingKeyMySQLRootPassword)
@@ -410,9 +418,11 @@ func (r *backupRepo) restoreMySQL(backup, target string) error {
if err != nil {
return err
}
defer mysql.Close()
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
if exist, _ := mysql.DatabaseExists(target); !exist {
return fmt.Errorf("数据库不存在:%s", target)
return errors.New(r.t.Get("database does not exist: %s", target))
}
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
return err
@@ -443,16 +453,18 @@ func (r *backupRepo) restoreMySQL(backup, target string) error {
// restorePostgres 恢复 PostgreSQL 备份
func (r *backupRepo) restorePostgres(backup, target string) error {
if !io.Exists(backup) {
return errors.New("备份文件不存在")
return errors.New(r.t.Get("backup file %s not exists", backup))
}
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", 5432)
if err != nil {
return err
}
defer postgres.Close()
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
if exist, _ := postgres.DatabaseExist(target); !exist {
return fmt.Errorf("数据库不存在:%s", target)
return errors.New(r.t.Get("database does not exist: %s", target))
}
clean := false
@@ -493,18 +505,19 @@ func (r *backupRepo) preCheckPath(to, path string) error {
}
if app.IsCli {
fmt.Printf("|-目标大小:%s\n", tools.FormatBytes(float64(size)))
fmt.Printf("|-目标文件数:%d\n", files)
fmt.Printf("|-备份目录可用空间:%s\n", tools.FormatBytes(float64(usage.Free)))
fmt.Printf("|-备份目录可用Inode%d\n", usage.InodesFree)
fmt.Println(r.t.Get("|-Target size: %s", tools.FormatBytes(float64(size))))
fmt.Println(r.t.Get("|-Target file count: %d", files))
fmt.Println(r.t.Get("|-Backup directory available space: %s", tools.FormatBytes(float64(usage.Free))))
fmt.Println(r.t.Get("|-Backup directory available Inode: %d", usage.InodesFree))
}
if uint64(size) > usage.Free {
return errors.New("备份目录空间不足")
}
if uint64(files) > usage.InodesFree {
return errors.New("备份目录Inode不足")
return errors.New(r.t.Get("Insufficient backup directory space"))
}
// 对于 fuse 等文件系统,可能没有 inode 的概念
/*if uint64(files) > usage.InodesFree {
return errors.New(r.t.Get("Insufficient backup directory inode"))
}*/
return nil
}
@@ -519,13 +532,13 @@ func (r *backupRepo) preCheckDB(to string, size int64) error {
}
if app.IsCli {
fmt.Printf("|-目标大小:%s\n", tools.FormatBytes(float64(size)))
fmt.Printf("|-备份目录可用空间:%s\n", tools.FormatBytes(float64(usage.Free)))
fmt.Printf("|-备份目录可用Inode%d\n", usage.InodesFree)
fmt.Println(r.t.Get("|-Target size: %s", tools.FormatBytes(float64(size))))
fmt.Println(r.t.Get("|-Backup directory available space: %s", tools.FormatBytes(float64(usage.Free))))
fmt.Println(r.t.Get("|-Backup directory available Inode: %d", usage.InodesFree))
}
if uint64(size) > usage.Free {
return errors.New("备份目录空间不足")
return errors.New(r.t.Get("Insufficient backup directory space"))
}
return nil
@@ -545,7 +558,7 @@ func (r *backupRepo) autoUnCompressSQL(backup string) (string, error) {
backup = "" // 置空,防止干扰后续判断
if files, err := os.ReadDir(temp); err == nil {
if len(files) != 1 {
return "", fmt.Errorf("压缩文件中包含的文件数量不为1,实际为%d", len(files))
return "", errors.New(r.t.Get("The number of files contained in the compressed file is not 1, actual %d", len(files)))
}
if strings.HasSuffix(files[0].Name(), ".sql") {
backup = filepath.Join(temp, files[0].Name())
@@ -553,7 +566,7 @@ func (r *backupRepo) autoUnCompressSQL(backup string) (string, error) {
}
if backup == "" {
return "", errors.New("无法找到.sql备份文件")
return "", errors.New(r.t.Get("could not find .sql backup file"))
}
return backup, nil
@@ -561,23 +574,14 @@ func (r *backupRepo) autoUnCompressSQL(backup string) (string, error) {
func (r *backupRepo) FixPanel() error {
if app.IsCli {
fmt.Println("|-开始修复面板...")
fmt.Println(r.t.Get("|-Start fixing the panel..."))
}
// 检查关键文件是否正常
flag := false
if !io.Exists("/usr/local/etc/panel/config.yml") {
flag = true
}
if !io.Exists(filepath.Join(app.Root, "panel", "web")) {
flag = true
}
if !io.Exists(filepath.Join(app.Root, "panel", "storage", "app.db")) {
flag = true
}
if io.Exists("/tmp/panel-storage.zip") {
flag = true
}
flag := !io.Exists("/usr/local/etc/panel/config.yml") ||
!io.Exists(filepath.Join(app.Root, "panel", "web")) ||
!io.Exists(filepath.Join(app.Root, "panel", "storage", "app.db")) ||
io.Exists("/tmp/panel-storage.zip")
// 检查数据库连接
if err := r.db.Exec("VACUUM").Error; err != nil {
flag = true
@@ -586,7 +590,7 @@ func (r *backupRepo) FixPanel() error {
flag = true
}
if !flag {
return fmt.Errorf("文件正常无需修复,请运行 panel-cli update 更新面板")
return errors.New(r.t.Get("Files are normal and do not need to be repaired, please run panel-cli update to update the panel"))
}
// 再次确认是否需要修复
@@ -596,10 +600,10 @@ func (r *backupRepo) FixPanel() error {
io.Exists(filepath.Join(app.Root, "panel", "storage", "app.db")) &&
io.Exists("/usr/local/etc/panel/config.yml") {
if err := io.Remove("/tmp/panel-storage.zip"); err != nil {
return fmt.Errorf("清理临时文件失败:%w", err)
return errors.New(r.t.Get("failed to clean temporary files: %v", err))
}
if app.IsCli {
fmt.Println("|-已清理临时文件,请运行 panel-cli update 更新面板")
fmt.Println(r.t.Get("|-Cleaned up temporary files, please run panel-cli update to update the panel"))
}
return nil
}
@@ -614,57 +618,57 @@ func (r *backupRepo) FixPanel() error {
return int(b.Time.Unix() - a.Time.Unix())
})
if len(list) == 0 {
return fmt.Errorf("未找到备份文件,无法自动修复")
return errors.New(r.t.Get("No backup file found, unable to automatically repair"))
}
latest := list[0]
if app.IsCli {
fmt.Printf("|-使用备份文件:%s\n", latest.Name)
fmt.Println(r.t.Get("|-Backup file used: %s", latest.Name))
}
// 解压备份文件
if app.IsCli {
fmt.Println("|-解压备份文件...")
fmt.Println(r.t.Get("|-Unzip backup file..."))
}
if err = io.Remove("/tmp/panel-fix"); err != nil {
return fmt.Errorf("清理临时目录失败:%w", err)
return errors.New(r.t.Get("Cleaning temporary directory failed: %v", err))
}
if err = io.UnCompress(latest.Path, "/tmp/panel-fix"); err != nil {
return fmt.Errorf("解压备份文件失败:%w", err)
return errors.New(r.t.Get("Unzip backup file failed: %v", err))
}
// 移动文件到对应位置
if app.IsCli {
fmt.Println("|-移动备份文件...")
fmt.Println(r.t.Get("|-Move backup file..."))
}
if io.Exists(filepath.Join("/tmp/panel-fix", "panel")) && io.IsDir(filepath.Join("/tmp/panel-fix", "panel")) {
if err = io.Remove(filepath.Join(app.Root, "panel")); err != nil {
return fmt.Errorf("删除目录失败:%w", err)
return errors.New(r.t.Get("Remove panel file failed: %v", err))
}
if err = io.Mv(filepath.Join("/tmp/panel-fix", "panel"), filepath.Join(app.Root)); err != nil {
return fmt.Errorf("移动目录失败:%w", err)
return errors.New(r.t.Get("Move panel file failed: %v", err))
}
}
if io.Exists(filepath.Join("/tmp/panel-fix", "config.yml")) {
if err = io.Mv(filepath.Join("/tmp/panel-fix", "config.yml"), "/usr/local/etc/panel/config.yml"); err != nil {
return fmt.Errorf("移动文件失败:%w", err)
return errors.New(r.t.Get("Move panel config failed: %v", err))
}
}
if io.Exists(filepath.Join("/tmp/panel-fix", "panel-cli")) {
if err = io.Mv(filepath.Join("/tmp/panel-fix", "panel-cli"), "/usr/local/sbin/panel-cli"); err != nil {
return fmt.Errorf("移动文件失败:%w", err)
return errors.New(r.t.Get("Move panel-cli file failed: %v", err))
}
}
// tmp 目录下如果有 storage 备份,则解压回去
if app.IsCli {
fmt.Println("|-恢复面板数据...")
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 fmt.Errorf("恢复面板数据失败:%w", err)
return errors.New(r.t.Get("Unzip panel data failed: %v", err))
}
if err = io.Remove("/tmp/panel-storage.zip"); err != nil {
return fmt.Errorf("清理临时文件失败:%w", err)
return errors.New(r.t.Get("Cleaning temporary file failed: %v", err))
}
}
@@ -677,7 +681,7 @@ func (r *backupRepo) FixPanel() error {
// 处理权限
if app.IsCli {
fmt.Println("|-设置关键文件权限...")
fmt.Println(r.t.Get("|-Set key file permissions..."))
}
if err = io.Chmod("/usr/local/etc/panel/config.yml", 0600); err != nil {
return err
@@ -697,7 +701,7 @@ func (r *backupRepo) FixPanel() error {
}
if app.IsCli {
fmt.Println("|-修复完成")
fmt.Println(r.t.Get("|-Fix completed"))
}
tools.RestartPanel()
@@ -715,124 +719,115 @@ func (r *backupRepo) UpdatePanel(version, url, checksum string) error {
name := filepath.Base(url)
if app.IsCli {
fmt.Printf("|-目标版本:%s\n", version)
fmt.Printf("|-下载链接:%s\n", url)
fmt.Printf("|-文件名:%s\n", name)
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("|-正在下载...")
fmt.Println(r.t.Get("|-Downloading..."))
}
if _, err := shell.Execf("wget -T 120 -t 3 -O /tmp/%s %s", name, url); err != nil {
return fmt.Errorf("下载失败:%w", err)
return errors.New(r.t.Get("Download failed: %v", err))
}
if _, err := shell.Execf("wget -T 20 -t 3 -O /tmp/%s %s", name+".sha256", checksum); err != nil {
return fmt.Errorf("下载失败:%w", err)
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("下载文件检查失败")
return errors.New(r.t.Get("Download file check failed"))
}
if app.IsCli {
fmt.Println("|-校验下载文件...")
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("下载文件校验失败")
return errors.New(r.t.Get("Verify download file failed: %v", err))
}
if err := io.Remove(filepath.Join("/tmp", name+".sha256")); err != nil {
if app.IsCli {
fmt.Println("|-清理校验文件失败:", err)
}
return fmt.Errorf("清理校验文件失败:%w", err)
return errors.New(r.t.Get("|-Clean up verification file failed: %v", err))
}
if app.IsCli {
fmt.Println("|-前置检查...")
}
if io.Exists("/tmp/panel-storage.zip") {
return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败所致,请运行 panel-cli fix 修复后重试")
return errors.New(r.t.Get("Temporary file detected in /tmp, this may be caused by the last update failure, please run panel-cli fix to repair and try again"))
}
if app.IsCli {
fmt.Println("|-备份面板数据...")
fmt.Println(r.t.Get("|-Backup panel data..."))
}
// 备份面板
if err := r.Create(biz.BackupTypePanel, ""); err != nil {
if app.IsCli {
fmt.Println("|-备份面板失败:", err)
}
return fmt.Errorf("备份面板失败:%w", err)
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 {
if app.IsCli {
fmt.Println("|-备份面板数据失败:", err)
}
return fmt.Errorf("备份面板数据失败:%w", err)
return errors.New(r.t.Get("|-Backup panel data failed: %v", err))
}
if !io.Exists("/tmp/panel-storage.zip") {
return errors.New("已备份面板数据检查失败")
return errors.New(r.t.Get("|-Backup panel data failed, missing file"))
}
if app.IsCli {
fmt.Println("|-清理旧版本...")
fmt.Println(r.t.Get("|-Cleaning old version..."))
}
if _, err := shell.Execf("rm -rf %s/panel/*", app.Root); err != nil {
return fmt.Errorf("清理旧版本失败:%w", err)
return errors.New(r.t.Get("|-Cleaning old version failed: %v", err))
}
if app.IsCli {
fmt.Println("|-解压新版本...")
fmt.Println(r.t.Get("|-Unzip new version..."))
}
if err := io.UnCompress(filepath.Join("/tmp", name), filepath.Join(app.Root, "panel")); err != nil {
return fmt.Errorf("解压失败:%w", err)
return errors.New(r.t.Get("|-Unzip new version failed: %v", err))
}
if !io.Exists(filepath.Join(app.Root, "panel", "web")) {
return errors.New("解压失败,缺失文件")
return errors.New(r.t.Get("|-Unzip new version failed, missing file"))
}
if err := io.Remove(filepath.Join("/tmp", name)); err != nil {
return fmt.Errorf("清理临时文件失败:%w", err)
return errors.New(r.t.Get("|-Clean up temporary file failed: %v", err))
}
if app.IsCli {
fmt.Println("|-恢复面板数据...")
fmt.Println(r.t.Get("|-Restore panel data..."))
}
if err := io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel", "storage")); err != nil {
return fmt.Errorf("恢复面板数据失败:%w", err)
return errors.New(r.t.Get("|-Restore panel data failed: %v", err))
}
if !io.Exists(filepath.Join(app.Root, "panel/storage/app.db")) {
return errors.New("恢复面板数据失败")
return errors.New(r.t.Get("|-Restore panel data failed, missing file"))
}
if app.IsCli {
fmt.Println("|-运行更新后脚本...")
fmt.Println(r.t.Get("|-Run post-update script..."))
}
if _, err := shell.Execf("curl -fsLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
return fmt.Errorf("运行面板更新后脚本失败:%w", err)
return errors.New(r.t.Get("|-Run post-update script failed: %v", err))
}
if _, err := shell.Execf(`wget -O /etc/systemd/system/panel.service https://dl.cdn.haozi.net/panel/panel.service && sed -i "s|/www|%s|g" /etc/systemd/system/panel.service`, app.Root); err != nil {
return fmt.Errorf("下载面板服务文件失败:%w", err)
return errors.New(r.t.Get("|-Download panel service file failed: %v", err))
}
if _, err := shell.Execf("panel-cli setting write version %s", version); err != nil {
return fmt.Errorf("写入面板版本号失败:%w", err)
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/panel-cli"); err != nil {
return fmt.Errorf("移动面板命令行工具失败:%w", err)
return errors.New(r.t.Get("|-Move panel-cli tool failed: %v", err))
}
if app.IsCli {
fmt.Println("|-设置关键文件权限...")
fmt.Println(r.t.Get("|-Set key file permissions..."))
}
_ = io.Chmod("/usr/local/sbin/panel-cli", 0700)
_ = io.Chmod("/etc/systemd/system/panel.service", 0644)
_ = io.Chmod(filepath.Join(app.Root, "panel"), 0700)
if app.IsCli {
fmt.Println("|-更新完成")
fmt.Println(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
+1 -1
View File
@@ -20,7 +20,7 @@ type cacheRepo struct {
func NewCacheRepo(db *gorm.DB) biz.CacheRepo {
return &cacheRepo{
api: api.NewAPI(app.Version),
api: api.NewAPI(app.Version, app.Locale),
db: db,
}
}
+74 -17
View File
@@ -4,10 +4,13 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"slices"
"strings"
"time"
"github.com/leonelquinteros/gotext"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/app"
@@ -22,13 +25,17 @@ import (
)
type certRepo struct {
t *gotext.Locale
db *gorm.DB
log *slog.Logger
client *acme.Client
}
func NewCertRepo(db *gorm.DB) biz.CertRepo {
func NewCertRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger) biz.CertRepo {
return &certRepo{
db: db,
t: t,
db: db,
log: log,
}
}
@@ -49,6 +56,8 @@ func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) {
AutoRenew: cert.AutoRenew,
Cert: cert.Cert,
Key: cert.Key,
CertURL: cert.CertURL,
Script: cert.Script,
CreatedAt: cert.CreatedAt,
UpdatedAt: cert.UpdatedAt,
}
@@ -80,10 +89,10 @@ func (r *certRepo) GetByWebsite(WebsiteID uint) (*biz.Cert, error) {
func (r *certRepo) Upload(req *request.CertUpload) (*biz.Cert, error) {
info, err := pkgcert.ParseCert(req.Cert)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %v", err)
return nil, errors.New(r.t.Get("failed to parse certificate: %v", err))
}
if _, err = pkgcert.ParseKey(req.Key); err != nil {
return nil, fmt.Errorf("failed to parse private key: %v", err)
return nil, errors.New(r.t.Get("failed to parse private key: %v", err))
}
cert := &biz.Cert{
@@ -120,7 +129,7 @@ func (r *certRepo) Update(req *request.CertUpdate) error {
req.Domains = info.DNSNames
}
if req.Type == "upload" && req.AutoRenew {
return errors.New("upload certificate cannot be set to auto renew")
return errors.New(r.t.Get("upload certificate cannot be set to auto renew"))
}
return r.db.Model(&biz.Cert{}).Where("id = ?", req.ID).Select("*").Updates(&biz.Cert{
@@ -131,6 +140,7 @@ func (r *certRepo) Update(req *request.CertUpdate) error {
Type: req.Type,
Cert: req.Cert,
Key: req.Key,
Script: req.Script,
Domains: req.Domains,
AutoRenew: req.AutoRenew,
}).Error
@@ -152,14 +162,14 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
}
if cert.DNS != nil {
client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data)
client.UseDns(cert.DNS.Type, cert.DNS.Data)
} else {
if cert.Website == nil {
return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually")
return nil, errors.New(r.t.Get("this certificate is not associated with a website and cannot be obtained. You can try to obtain it manually"))
} else {
for _, domain := range cert.Domains {
if strings.Contains(domain, "*") {
return nil, errors.New("wildcard domains cannot use HTTP verification")
return nil, errors.New(r.t.Get("wildcard domains cannot use HTTP verification"))
}
}
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
@@ -183,6 +193,10 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
return &ssl, r.Deploy(cert.ID, cert.WebsiteID)
}
if err = r.runScript(cert); err != nil {
return nil, err
}
return &ssl, nil
}
@@ -193,7 +207,7 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
}
if r.client == nil {
return nil, errors.New("please retry the manual obtain operation")
return nil, errors.New(r.t.Get("please retry the manual obtain operation"))
}
ssl, err := r.client.ObtainCertificateManual()
@@ -212,6 +226,10 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
return &ssl, r.Deploy(cert.ID, cert.WebsiteID)
}
if err = r.runScript(cert); err != nil {
return nil, err
}
return &ssl, nil
}
@@ -236,6 +254,10 @@ func (r *certRepo) ObtainSelfSigned(id uint) error {
return r.Deploy(cert.ID, cert.WebsiteID)
}
if err = r.runScript(cert); err != nil {
return err
}
return nil
}
@@ -251,18 +273,18 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
}
if cert.CertURL == "" {
return nil, errors.New("this certificate has not been signed successfully and cannot be renewed")
return nil, errors.New(r.t.Get("this certificate has not been obtained successfully and cannot be renewed"))
}
if cert.DNS != nil {
client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data)
client.UseDns(cert.DNS.Type, cert.DNS.Data)
} else {
if cert.Website == nil {
return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually")
return nil, errors.New(r.t.Get("this certificate is not associated with a website and cannot be obtained. You can try to obtain it manually"))
} else {
for _, domain := range cert.Domains {
if strings.Contains(domain, "*") {
return nil, errors.New("wildcard domains cannot use HTTP verification")
return nil, errors.New(r.t.Get("wildcard domains cannot use HTTP verification"))
}
}
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
@@ -272,7 +294,11 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
ssl, err := client.RenewCertificate(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
if err != nil {
return nil, err
// 续签失败,尝试重签
ssl, err = client.ObtainCertificate(context.Background(), cert.Domains, acme.KeyType(cert.Type))
if err != nil {
return nil, err
}
}
cert.CertURL = ssl.URL
@@ -322,7 +348,7 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
}
if cert.Cert == "" || cert.Key == "" {
return errors.New("this certificate has not been signed successfully and cannot be deployed")
return errors.New(r.t.Get("this certificate has not been obtained successfully and cannot be deployed"))
}
website := new(biz.Website)
@@ -344,9 +370,40 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
return nil
}
func (r *certRepo) runScript(cert *biz.Cert) error {
if cert.Script == "" {
return nil
}
f, err := os.CreateTemp("", "cert-deploy-*.sh")
if err != nil {
return err
}
// 替换变量
cert.Script = strings.ReplaceAll(cert.Script, "{cert}", cert.Cert)
cert.Script = strings.ReplaceAll(cert.Script, "{key}", cert.Key)
if _, err = f.WriteString(cert.Script); err != nil {
return err
}
if err = f.Chmod(0755); err != nil {
return err
}
if err = f.Close(); err != nil {
return err
}
defer func(name string) {
_ = os.Remove(name)
}(f.Name())
_, err = shell.Execf("bash " + f.Name())
return err
}
func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) {
if cert.Account == nil {
return nil, errors.New("this certificate is not associated with an ACME account and cannot be signed")
return nil, errors.New(r.t.Get("this certificate is not associated with an ACME account and cannot be obtained"))
}
var ca string
@@ -370,5 +427,5 @@ func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) {
eab = &acme.EAB{KeyID: cert.Account.Kid, MACKey: cert.Account.HmacEncoded}
}
return acme.NewPrivateKeyAccount(cert.Account.Email, cert.Account.PrivateKey, ca, eab)
return acme.NewPrivateKeyAccount(cert.Account.Email, cert.Account.PrivateKey, ca, eab, r.log)
}
+35 -30
View File
@@ -3,10 +3,11 @@ package data
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"github.com/go-resty/resty/v2"
"github.com/leonelquinteros/gotext"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/biz"
@@ -16,19 +17,23 @@ import (
)
type certAccountRepo struct {
t *gotext.Locale
db *gorm.DB
log *slog.Logger
user biz.UserRepo
}
func NewCertAccountRepo(db *gorm.DB, user biz.UserRepo) biz.CertAccountRepo {
func NewCertAccountRepo(t *gotext.Locale, db *gorm.DB, user biz.UserRepo, log *slog.Logger) biz.CertAccountRepo {
return &certAccountRepo{
t: t,
db: db,
log: log,
user: user,
}
}
func (r certAccountRepo) List(page, limit uint) ([]*biz.CertAccount, int64, error) {
var accounts []*biz.CertAccount
accounts := make([]*biz.CertAccount, 0)
var total int64
err := r.db.Model(&biz.CertAccount{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&accounts).Error
return accounts, total, err
@@ -41,12 +46,12 @@ func (r certAccountRepo) GetDefault(userID uint) (*biz.CertAccount, error) {
}
account := new(biz.CertAccount)
if err = r.db.Model(&biz.CertAccount{}).Where("ca = ?", "googlecn").Where("email = ?", user.Email).First(account).Error; err == nil {
if err = r.db.Model(&biz.CertAccount{}).Where("ca = ?", "letsencrypt").Where("email = ?", user.Email).First(account).Error; err == nil {
return account, nil
}
req := &request.CertAccountCreate{
CA: "googlecn",
CA: "letsencrypt",
Email: user.Email,
KeyType: string(acme.KeyEC256),
}
@@ -78,13 +83,13 @@ func (r certAccountRepo) Create(req *request.CertAccountCreate) (*biz.CertAccoun
}
account.Kid = eab.KeyID
account.HmacEncoded = eab.MACKey
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogleCN, eab, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogleCN, eab, acme.KeyType(account.KeyType), r.log)
case "google":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogle, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogle, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType), r.log)
case "letsencrypt":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType), r.log)
case "buypass":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CABuypass, nil, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CABuypass, nil, acme.KeyType(account.KeyType), r.log)
case "zerossl":
eab, eabErr := r.getZeroSSLEAB(account.Email)
if eabErr != nil {
@@ -92,20 +97,20 @@ func (r certAccountRepo) Create(req *request.CertAccountCreate) (*biz.CertAccoun
}
account.Kid = eab.KeyID
account.HmacEncoded = eab.MACKey
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAZeroSSL, eab, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAZeroSSL, eab, acme.KeyType(account.KeyType), r.log)
case "sslcom":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType), r.log)
default:
return nil, errors.New("unsupported CA")
return nil, errors.New(r.t.Get("unsupported CA"))
}
if err != nil {
return nil, fmt.Errorf("failed to register account: %v", err)
return nil, errors.New(r.t.Get("failed to register account: %v", err))
}
privateKey, err := cert.EncodeKey(client.Account.PrivateKey)
if err != nil {
return nil, errors.New("failed to get private key")
return nil, errors.New(r.t.Get("failed to get private key"))
}
account.PrivateKey = string(privateKey)
@@ -137,13 +142,13 @@ func (r certAccountRepo) Update(req *request.CertAccountUpdate) error {
}
account.Kid = eab.KeyID
account.HmacEncoded = eab.MACKey
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogleCN, eab, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogleCN, eab, acme.KeyType(account.KeyType), r.log)
case "google":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogle, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAGoogle, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType), r.log)
case "letsencrypt":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CALetsEncrypt, nil, acme.KeyType(account.KeyType), r.log)
case "buypass":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CABuypass, nil, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CABuypass, nil, acme.KeyType(account.KeyType), r.log)
case "zerossl":
eab, eabErr := r.getZeroSSLEAB(account.Email)
if eabErr != nil {
@@ -151,20 +156,20 @@ func (r certAccountRepo) Update(req *request.CertAccountUpdate) error {
}
account.Kid = eab.KeyID
account.HmacEncoded = eab.MACKey
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAZeroSSL, eab, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CAZeroSSL, eab, acme.KeyType(account.KeyType), r.log)
case "sslcom":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType), r.log)
default:
return errors.New("unsupported CA")
return errors.New(r.t.Get("unsupported CA"))
}
if err != nil {
return errors.New("failed to register account")
return errors.New(r.t.Get("failed to register account: %v", err))
}
privateKey, err := cert.EncodeKey(client.Account.PrivateKey)
if err != nil {
return errors.New("failed to get private key")
return errors.New(r.t.Get("failed to get private key: %v", err))
}
account.PrivateKey = string(privateKey)
@@ -178,8 +183,8 @@ func (r certAccountRepo) Delete(id uint) error {
// getGoogleEAB 获取 Google EAB
func (r certAccountRepo) getGoogleEAB() (*acme.EAB, error) {
type data struct {
Message string `json:"message"`
Data struct {
Msg string `json:"msg"`
Data struct {
KeyId string `json:"key_id"`
MacKey string `json:"mac_key"`
} `json:"data"`
@@ -190,11 +195,11 @@ func (r certAccountRepo) getGoogleEAB() (*acme.EAB, error) {
resp, err := client.R().SetResult(&data{}).Get("https://gts.rat.dev/eab")
if err != nil || !resp.IsSuccess() {
return &acme.EAB{}, fmt.Errorf("failed to get Google EAB: %v", err)
return &acme.EAB{}, errors.New(r.t.Get("failed to get Google EAB: %v", err))
}
eab := resp.Result().(*data)
if eab.Message != "success" {
return &acme.EAB{}, fmt.Errorf("failed to get Google EAB: %s", eab.Message)
if eab.Msg != "success" {
return &acme.EAB{}, errors.New(r.t.Get("failed to get Google EAB: %s", eab.Msg))
}
return &acme.EAB{KeyID: eab.Data.KeyId, MACKey: eab.Data.MacKey}, nil
@@ -215,11 +220,11 @@ func (r certAccountRepo) getZeroSSLEAB(email string) (*acme.EAB, error) {
"email": email,
}).SetResult(&data{}).Post("https://api.zerossl.com/acme/eab-credentials-email")
if err != nil || !resp.IsSuccess() {
return &acme.EAB{}, fmt.Errorf("failed to get ZeroSSL EAB: %v", err)
return &acme.EAB{}, errors.New(r.t.Get("failed to get ZeroSSL EAB: %v", err))
}
eab := resp.Result().(*data)
if !eab.Success {
return &acme.EAB{}, fmt.Errorf("failed to get ZeroSSL EAB")
return &acme.EAB{}, errors.New(r.t.Get("failed to get ZeroSSL EAB"))
}
return &acme.EAB{KeyID: eab.EabKid, MACKey: eab.EabHmacKey}, nil
+1 -1
View File
@@ -18,7 +18,7 @@ func NewCertDNSRepo(db *gorm.DB) biz.CertDNSRepo {
}
func (r certDNSRepo) List(page, limit uint) ([]*biz.CertDNS, int64, error) {
var certDNS []*biz.CertDNS
certDNS := make([]*biz.CertDNS, 0)
var total int64
err := r.db.Model(&biz.CertDNS{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certDNS).Error
return certDNS, total, err
+2 -2
View File
@@ -39,7 +39,7 @@ func (r *containerRepo) ListAll() ([]types.Container, error) {
for _, port := range item.Ports {
ports = append(ports, types.ContainerPort{
ContainerStart: uint(port.PrivatePort),
ContainerEnd: uint(port.PublicPort),
ContainerEnd: uint(port.PrivatePort),
HostStart: uint(port.PublicPort),
HostEnd: uint(port.PublicPort),
Protocol: port.Type,
@@ -200,7 +200,7 @@ func (r *containerRepo) Rename(id string, newName string) error {
// Logs 查看容器日志
func (r *containerRepo) Logs(id string) (string, error) {
return shell.ExecfWithTimeout(2*time.Minute, "docker logs %s", id)
return shell.ExecfWithTimeout(2*time.Minute, "docker logs --tail 100 %s", id)
}
// Prune 清理未使用的容器
+160
View File
@@ -0,0 +1,160 @@
package data
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"time"
"github.com/tnb-labs/panel/internal/app"
"github.com/tnb-labs/panel/internal/biz"
"github.com/tnb-labs/panel/pkg/shell"
"github.com/tnb-labs/panel/pkg/types"
)
type containerComposeRepo struct{}
func NewContainerComposeRepo() biz.ContainerComposeRepo {
return &containerComposeRepo{}
}
// List 列出所有编排文件名
func (r *containerComposeRepo) List() ([]types.ContainerCompose, error) {
raw, err := shell.Execf("docker compose ls -a --format json")
if err != nil {
return nil, err
}
var composeRaws []types.ContainerComposeRaw
if err = json.Unmarshal([]byte(raw), &composeRaws); err != nil {
return nil, err
}
composeDir := filepath.Join(app.Root, "server", "compose")
entries, err := os.ReadDir(composeDir)
if err != nil {
return nil, err
}
var composes []types.ContainerCompose
index := make(map[string]int)
for _, entry := range entries {
if !entry.IsDir() {
continue
}
path := filepath.Join(composeDir, entry.Name())
var createdAt time.Time
if info, err := entry.Info(); err == nil {
createdAt = info.ModTime()
}
composes = append(composes, types.ContainerCompose{
Name: entry.Name(),
Path: path,
Status: "unknown",
CreatedAt: createdAt,
})
index[entry.Name()] = len(composes) - 1
}
// 更新状态
for _, item := range composeRaws {
if i, ok := index[item.Name]; ok {
composes[i].Status = item.Status
}
}
return composes, nil
}
// Get 获取编排文件和环境变量内容
func (r *containerComposeRepo) Get(name string) (string, []types.KV, error) {
content, _ := os.ReadFile(filepath.Join(app.Root, "server", "compose", name, "docker-compose.yml"))
env, _ := os.ReadFile(filepath.Join(app.Root, "server", "compose", name, ".env"))
var envs []types.KV
for _, line := range strings.Split(string(env), "\n") {
if line == "" {
continue
}
kv := strings.SplitN(line, "=", 2)
if len(kv) != 2 {
continue
}
envs = append(envs, types.KV{Key: kv[0], Value: kv[1]})
}
return string(content), envs, nil // 有意忽略错误,这样可以允许新建文件
}
// Create 创建编排文件
func (r *containerComposeRepo) Create(name, compose string, envs []types.KV) error {
dir := filepath.Join(app.Root, "server", "compose", name)
if err := os.MkdirAll(dir, 0644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(dir, "docker-compose.yml"), []byte(compose), 0644); err != nil {
return err
}
var sb strings.Builder
for _, kv := range envs {
sb.WriteString(kv.Key)
sb.WriteString("=")
sb.WriteString(kv.Value)
sb.WriteString("\n")
}
if err := os.WriteFile(filepath.Join(dir, ".env"), []byte(sb.String()), 0644); err != nil {
return err
}
return nil
}
// Update 更新编排文件
func (r *containerComposeRepo) Update(name, compose string, envs []types.KV) error {
dir := filepath.Join(app.Root, "server", "compose", name)
if err := os.WriteFile(filepath.Join(dir, "docker-compose.yml"), []byte(compose), 0644); err != nil {
return err
}
var sb strings.Builder
for _, kv := range envs {
sb.WriteString(kv.Key)
sb.WriteString("=")
sb.WriteString(kv.Value)
sb.WriteString("\n")
}
if err := os.WriteFile(filepath.Join(dir, ".env"), []byte(sb.String()), 0644); err != nil {
return err
}
return nil
}
// Up 启动编排
func (r *containerComposeRepo) Up(name string, force bool) error {
file := filepath.Join(app.Root, "server", "compose", name, "docker-compose.yml")
cmd := "docker compose -f %s up -d"
if force {
cmd += " --pull always" // 强制拉取镜像
}
_, err := shell.Execf(cmd, file)
return err
}
// Down 停止编排
func (r *containerComposeRepo) Down(name string) error {
file := filepath.Join(app.Root, "server", "compose", name, "docker-compose.yml")
_, err := shell.Execf("docker compose -f %s down", file)
return err
}
// Remove 删除编排
func (r *containerComposeRepo) Remove(name string) error {
dir := filepath.Join(app.Root, "server", "compose", name)
return os.RemoveAll(dir)
}
+10 -19
View File
@@ -8,6 +8,7 @@ import (
"time"
"github.com/go-rat/utils/str"
"github.com/leonelquinteros/gotext"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/app"
@@ -20,11 +21,13 @@ import (
)
type cronRepo struct {
t *gotext.Locale
db *gorm.DB
}
func NewCronRepo(db *gorm.DB) biz.CronRepo {
func NewCronRepo(t *gotext.Locale, db *gorm.DB) biz.CronRepo {
return &cronRepo{
t: t,
db: db,
}
}
@@ -39,7 +42,7 @@ func (r *cronRepo) Count() (int64, error) {
}
func (r *cronRepo) List(page, limit uint) ([]*biz.Cron, int64, error) {
var cron []*biz.Cron
cron := make([]*biz.Cron, 0)
var total int64
err := r.db.Model(&biz.Cron{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&cron).Error
return cron, total, err
@@ -61,8 +64,6 @@ func (r *cronRepo) Create(req *request.CronCreate) error {
script = fmt.Sprintf(`#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子面板 - 网站备份脚本
panel-cli backup website -n '%s' -p '%s'
panel-cli backup clear -t website -f '%s' -s '%d' -p '%s'
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
@@ -71,8 +72,6 @@ panel-cli backup clear -t website -f '%s' -s '%d' -p '%s'
script = fmt.Sprintf(`#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子面板 - 数据库备份脚本
panel-cli backup database -t '%s' -n '%s' -p '%s'
panel-cli backup clear -t '%s' -f '%s' -s '%d' -p '%s'
`, req.BackupType, req.Target, req.BackupPath, req.BackupType, req.Target, req.Save, req.BackupPath)
@@ -82,9 +81,6 @@ panel-cli backup clear -t '%s' -f '%s' -s '%d' -p '%s'
script = fmt.Sprintf(`#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子面板 - 日志切割脚本
# 执行切割
panel-cli cutoff website -n '%s' -p '%s'
panel-cli cutoff clear -t website -f '%s' -s '%d' -p '%s'
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
@@ -96,18 +92,17 @@ panel-cli cutoff clear -t website -f '%s' -s '%d' -p '%s'
shellDir := fmt.Sprintf("%s/server/cron/", app.Root)
shellLogDir := fmt.Sprintf("%s/server/cron/logs/", app.Root)
if !io.Exists(shellDir) {
return errors.New("计划任务目录不存在")
return errors.New(r.t.Get("cron directory %s not exists", shellDir))
}
if !io.Exists(shellLogDir) {
return errors.New("计划任务日志目录不存在")
return errors.New(r.t.Get("cron log directory %s not exists", shellLogDir))
}
shellFile := strconv.Itoa(int(time.Now().Unix())) + str.Random(16)
if err := io.Write(filepath.Join(shellDir, shellFile+".sh"), script, 0700); err != nil {
return errors.New(err.Error())
}
if out, err := shell.Execf("dos2unix %s%s.sh", shellDir, shellFile); err != nil {
return errors.New(out)
}
// 编码转换
_, _ = shell.Execf("dos2unix %s%s.sh", shellDir, shellFile)
cron := new(biz.Cron)
cron.Name = req.Name
@@ -133,10 +128,6 @@ func (r *cronRepo) Update(req *request.CronUpdate) error {
return err
}
if !cron.Status {
return errors.New("计划任务已禁用")
}
cron.Time = req.Time
cron.Name = req.Name
if err = r.db.Save(cron).Error; err != nil {
@@ -226,5 +217,5 @@ func (r *cronRepo) restartCron() error {
return systemctl.Restart("cron")
}
return errors.New("不支持的系统")
return errors.New(r.t.Get("unsupported system"))
}
+2
View File
@@ -11,6 +11,7 @@ var ProviderSet = wire.NewSet(
NewCertAccountRepo,
NewCertDNSRepo,
NewContainerRepo,
NewContainerComposeRepo,
NewContainerImageRepo,
NewContainerNetworkRepo,
NewContainerVolumeRepo,
@@ -24,5 +25,6 @@ var ProviderSet = wire.NewSet(
NewSSHRepo,
NewTaskRepo,
NewUserRepo,
NewUserTokenRepo,
NewWebsiteRepo,
)
+20 -7
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"slices"
"github.com/leonelquinteros/gotext"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/biz"
@@ -13,13 +14,15 @@ import (
)
type databaseRepo struct {
t *gotext.Locale
db *gorm.DB
server biz.DatabaseServerRepo
user biz.DatabaseUserRepo
}
func NewDatabaseRepo(db *gorm.DB, server biz.DatabaseServerRepo, user biz.DatabaseUserRepo) biz.DatabaseRepo {
func NewDatabaseRepo(t *gotext.Locale, db *gorm.DB, server biz.DatabaseServerRepo, user biz.DatabaseUserRepo) biz.DatabaseRepo {
return &databaseRepo{
t: t,
db: db,
server: server,
user: user,
@@ -86,7 +89,9 @@ func (r databaseRepo) Create(req *request.DatabaseCreate) error {
if err != nil {
return err
}
defer mysql.Close()
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
if req.CreateUser {
if err = r.user.Create(&request.DatabaseUserCreate{
ServerID: req.ServerID,
@@ -110,7 +115,9 @@ func (r databaseRepo) Create(req *request.DatabaseCreate) error {
if err != nil {
return err
}
defer postgres.Close()
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
if req.CreateUser {
if err = r.user.Create(&request.DatabaseUserCreate{
ServerID: req.ServerID,
@@ -149,14 +156,18 @@ func (r databaseRepo) Delete(serverID uint, name string) error {
if err != nil {
return err
}
defer mysql.Close()
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
return mysql.DatabaseDrop(name)
case biz.DatabaseTypePostgresql:
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
if err != nil {
return err
}
defer postgres.Close()
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
return postgres.DatabaseDrop(name)
}
@@ -171,13 +182,15 @@ func (r databaseRepo) Comment(req *request.DatabaseComment) error {
switch server.Type {
case biz.DatabaseTypeMysql:
return errors.New("mysql not support database comment")
return errors.New(r.t.Get("mysql not support database comment"))
case biz.DatabaseTypePostgresql:
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
if err != nil {
return err
}
defer postgres.Close()
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
return postgres.DatabaseComment(req.Name, req.Comment)
}
+16 -8
View File
@@ -1,10 +1,12 @@
package data
import (
"errors"
"fmt"
"log/slog"
"slices"
"github.com/leonelquinteros/gotext"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/biz"
@@ -13,12 +15,14 @@ import (
)
type databaseServerRepo struct {
t *gotext.Locale
db *gorm.DB
log *slog.Logger
}
func NewDatabaseServerRepo(db *gorm.DB, log *slog.Logger) biz.DatabaseServerRepo {
func NewDatabaseServerRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger) biz.DatabaseServerRepo {
return &databaseServerRepo{
t: t,
db: db,
log: log,
}
@@ -34,7 +38,7 @@ func (r databaseServerRepo) Count() (int64, error) {
}
func (r databaseServerRepo) List(page, limit uint) ([]*biz.DatabaseServer, int64, error) {
var databaseServer []*biz.DatabaseServer
databaseServer := make([]*biz.DatabaseServer, 0)
var total int64
err := r.db.Model(&biz.DatabaseServer{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&databaseServer).Error
@@ -79,7 +83,7 @@ func (r databaseServerRepo) Create(req *request.DatabaseServerCreate) error {
}
if !r.checkServer(databaseServer) {
return fmt.Errorf("check server connection failed")
return errors.New(r.t.Get("check server connection failed"))
}
return r.db.Create(databaseServer).Error
@@ -99,7 +103,7 @@ func (r databaseServerRepo) Update(req *request.DatabaseServerUpdate) error {
server.Remark = req.Remark
if !r.checkServer(server) {
return fmt.Errorf("check server connection failed")
return errors.New(r.t.Get("check server connection failed"))
}
return r.db.Save(server).Error
@@ -139,7 +143,9 @@ func (r databaseServerRepo) Sync(id uint) error {
if err != nil {
return err
}
defer mysql.Close()
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
allUsers, err := mysql.Users()
if err != nil {
return err
@@ -152,7 +158,7 @@ func (r databaseServerRepo) Sync(id uint) error {
ServerID: id,
Username: user.User,
Host: user.Host,
Remark: fmt.Sprintf("sync from server %s", server.Name),
Remark: r.t.Get("sync from server %s", server.Name),
}
if err = r.db.Create(newUser).Error; err != nil {
r.log.Warn("sync database user failed", slog.Any("err", err))
@@ -164,7 +170,9 @@ func (r databaseServerRepo) Sync(id uint) error {
if err != nil {
return err
}
defer postgres.Close()
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
allUsers, err := postgres.Users()
if err != nil {
return err
@@ -176,7 +184,7 @@ func (r databaseServerRepo) Sync(id uint) error {
newUser := &biz.DatabaseUser{
ServerID: id,
Username: user.Role,
Remark: fmt.Sprintf("sync from server %s", server.Name),
Remark: r.t.Get("sync from server %s", server.Name),
}
r.db.Create(newUser)
}
+35 -12
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"slices"
"github.com/leonelquinteros/gotext"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/biz"
@@ -12,12 +13,14 @@ import (
)
type databaseUserRepo struct {
t *gotext.Locale
db *gorm.DB
server biz.DatabaseServerRepo
}
func NewDatabaseUserRepo(db *gorm.DB, server biz.DatabaseServerRepo) biz.DatabaseUserRepo {
func NewDatabaseUserRepo(t *gotext.Locale, db *gorm.DB, server biz.DatabaseServerRepo) biz.DatabaseUserRepo {
return &databaseUserRepo{
t: t,
db: db,
server: server,
}
@@ -33,7 +36,7 @@ func (r databaseUserRepo) Count() (int64, error) {
}
func (r databaseUserRepo) List(page, limit uint) ([]*biz.DatabaseUser, int64, error) {
var user []*biz.DatabaseUser
user := make([]*biz.DatabaseUser, 0)
var total int64
err := r.db.Model(&biz.DatabaseUser{}).Preload("Server").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&user).Error
@@ -68,7 +71,9 @@ func (r databaseUserRepo) Create(req *request.DatabaseUserCreate) error {
if err != nil {
return err
}
defer mysql.Close()
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
if err = mysql.UserCreate(req.Username, req.Password, req.Host); err != nil {
return err
}
@@ -87,7 +92,9 @@ func (r databaseUserRepo) Create(req *request.DatabaseUserCreate) error {
if err != nil {
return err
}
defer postgres.Close()
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
if err = postgres.UserCreate(req.Username, req.Password); err != nil {
return err
}
@@ -129,7 +136,9 @@ func (r databaseUserRepo) Update(req *request.DatabaseUserUpdate) error {
if err != nil {
return err
}
defer mysql.Close()
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
if req.Password != "" {
if err = mysql.UserPassword(user.Username, req.Password, user.Host); err != nil {
return err
@@ -145,7 +154,9 @@ func (r databaseUserRepo) Update(req *request.DatabaseUserUpdate) error {
if err != nil {
return err
}
defer postgres.Close()
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
if req.Password != "" {
if err = postgres.UserPassword(user.Username, req.Password); err != nil {
return err
@@ -192,14 +203,18 @@ func (r databaseUserRepo) Delete(id uint) error {
if err != nil {
return err
}
defer mysql.Close()
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
_ = mysql.UserDrop(user.Username, user.Host)
case biz.DatabaseTypePostgresql:
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
if err != nil {
return err
}
defer postgres.Close()
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
_ = postgres.UserDrop(user.Username)
}
@@ -218,7 +233,9 @@ func (r databaseUserRepo) DeleteByNames(serverID uint, names []string) error {
if err != nil {
return err
}
defer mysql.Close()
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
users := make([]*biz.DatabaseUser, 0)
if err = r.db.Where("server_id = ? AND username IN ?", serverID, names).Find(&users).Error; err != nil {
return err
@@ -238,7 +255,9 @@ func (r databaseUserRepo) DeleteByNames(serverID uint, names []string) error {
if err != nil {
return err
}
defer postgres.Close()
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
for name := range slices.Values(names) {
_ = postgres.UserDrop(name)
}
@@ -254,7 +273,9 @@ func (r databaseUserRepo) fillUser(user *biz.DatabaseUser) {
case biz.DatabaseTypeMysql:
mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
if err == nil {
defer mysql.Close()
defer func(mysql *db.MySQL) {
_ = mysql.Close()
}(mysql)
privileges, _ := mysql.UserPrivileges(user.Username, user.Host)
user.Privileges = privileges
}
@@ -267,7 +288,9 @@ func (r databaseUserRepo) fillUser(user *biz.DatabaseUser) {
case biz.DatabaseTypePostgresql:
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
if err == nil {
defer postgres.Close()
defer func(postgres *db.Postgres) {
_ = postgres.Close()
}(postgres)
privileges, _ := postgres.UserPrivileges(user.Username)
user.Privileges = privileges
}
+1 -6
View File
@@ -1,7 +1,6 @@
package data
import (
"errors"
"time"
"github.com/spf13/cast"
@@ -56,14 +55,10 @@ func (r monitorRepo) Clear() error {
}
func (r monitorRepo) List(start, end time.Time) ([]*biz.Monitor, error) {
var monitors []*biz.Monitor
monitors := make([]*biz.Monitor, 0)
if err := r.db.Where("created_at BETWEEN ? AND ?", start, end).Find(&monitors).Error; err != nil {
return nil, err
}
if len(monitors) == 0 {
return nil, errors.New("没有找到数据")
}
return monitors, nil
}
+176 -58
View File
@@ -1,13 +1,13 @@
package data
import (
"context"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"sync"
"github.com/go-rat/utils/hash"
"github.com/knadh/koanf/v2"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"gopkg.in/yaml.v3"
"gorm.io/gorm"
@@ -23,13 +23,16 @@ import (
)
type settingRepo struct {
db *gorm.DB
conf *koanf.Koanf
task biz.TaskRepo
t *gotext.Locale
cache sync.Map
db *gorm.DB
conf *koanf.Koanf
task biz.TaskRepo
}
func NewSettingRepo(db *gorm.DB, conf *koanf.Koanf, task biz.TaskRepo) biz.SettingRepo {
func NewSettingRepo(t *gotext.Locale, db *gorm.DB, conf *koanf.Koanf, task biz.TaskRepo) biz.SettingRepo {
return &settingRepo{
t: t,
db: db,
conf: conf,
task: task,
@@ -37,6 +40,13 @@ func NewSettingRepo(db *gorm.DB, conf *koanf.Koanf, task biz.TaskRepo) biz.Setti
}
func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, error) {
if cache, ok := r.cache.Load(key); ok {
if v, ok := cache.(string); ok {
return v, nil
}
r.cache.Delete(key)
}
setting := new(biz.Setting)
if err := r.db.Where("key = ?", key).First(setting).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -52,6 +62,13 @@ func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, e
}
func (r *settingRepo) GetBool(key biz.SettingKey, defaultValue ...bool) (bool, error) {
if cache, ok := r.cache.Load(key); ok {
if v, ok := cache.(bool); ok {
return v, nil
}
r.cache.Delete(key)
}
setting := new(biz.Setting)
if err := r.db.Where("key = ?", key).First(setting).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -66,6 +83,62 @@ func (r *settingRepo) GetBool(key biz.SettingKey, defaultValue ...bool) (bool, e
return cast.ToBool(setting.Value), nil
}
func (r *settingRepo) GetInt(key biz.SettingKey, defaultValue ...int) (int, error) {
if cache, ok := r.cache.Load(key); ok {
if v, ok := cache.(int); ok {
return v, nil
}
r.cache.Delete(key)
}
setting := new(biz.Setting)
if err := r.db.Where("key = ?", key).First(setting).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return 0, err
}
}
if setting.Value == "" && len(defaultValue) > 0 {
return defaultValue[0], nil
}
return cast.ToInt(setting.Value), nil
}
func (r *settingRepo) GetSlice(key biz.SettingKey, defaultValue ...[]string) ([]string, error) {
if cache, ok := r.cache.Load(key); ok {
if v, ok := cache.([]string); ok {
return v, nil
}
r.cache.Delete(key)
}
setting := new(biz.Setting)
if err := r.db.Where("key = ?", key).First(setting).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
}
// 设置值为空时提前返回
slice := make([]string, 0)
if setting.Value == "" {
if len(defaultValue) > 0 {
return defaultValue[0], nil
}
return slice, nil
}
if err := json.Unmarshal([]byte(setting.Value), &slice); err != nil {
return nil, err
}
if len(slice) == 0 && len(defaultValue) > 0 {
return defaultValue[0], nil
}
return slice, nil
}
func (r *settingRepo) Set(key biz.SettingKey, value string) error {
setting := new(biz.Setting)
if err := r.db.Where("key = ?", key).First(setting).Error; err != nil {
@@ -76,7 +149,41 @@ func (r *settingRepo) Set(key biz.SettingKey, value string) error {
setting.Key = key
setting.Value = value
return r.db.Save(setting).Error
if err := r.db.Save(setting).Error; err != nil {
return err
}
r.cache.Store(key, value)
return nil
}
func (r *settingRepo) SetSlice(key biz.SettingKey, value []string) error {
setting := new(biz.Setting)
if err := r.db.Where("key = ?", key).First(setting).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
}
setting.Key = key
if len(value) == 0 {
setting.Value = "[]"
} else {
b, err := json.Marshal(value)
if err != nil {
return err
}
setting.Value = string(b)
}
if err := r.db.Save(setting).Error; err != nil {
return err
}
r.cache.Store(key, value)
return nil
}
func (r *settingRepo) Delete(key biz.SettingKey) error {
@@ -85,14 +192,20 @@ func (r *settingRepo) Delete(key biz.SettingKey) error {
return err
}
r.cache.Delete(key)
return nil
}
func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSetting, error) {
func (r *settingRepo) GetPanel() (*request.SettingPanel, error) {
name, err := r.Get(biz.SettingKeyName)
if err != nil {
return nil, err
}
channel, err := r.Get(biz.SettingKeyChannel)
if err != nil {
return nil, err
}
offlineMode, err := r.GetBool(biz.SettingKeyOfflineMode)
if err != nil {
return nil, err
@@ -110,12 +223,6 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
return nil, err
}
userID := cast.ToUint(ctx.Value("user_id"))
user := new(biz.User)
if err := r.db.Where("id = ?", userID).First(user).Error; err != nil {
return nil, err
}
crt, err := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
if err != nil {
return nil, err
@@ -125,16 +232,19 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
return nil, err
}
return &request.PanelSetting{
return &request.SettingPanel{
Name: name,
Channel: channel,
Locale: r.conf.String("app.locale"),
Entrance: r.conf.String("http.entrance"),
OfflineMode: offlineMode,
AutoUpdate: autoUpdate,
Lifetime: uint(r.conf.Int("session.lifetime")),
BindDomain: r.conf.Strings("http.bind_domain"),
BindIP: r.conf.Strings("http.bind_ip"),
BindUA: r.conf.Strings("http.bind_ua"),
WebsitePath: websitePath,
BackupPath: backupPath,
Username: user.Username,
Email: user.Email,
Port: uint(r.conf.Int("http.port")),
HTTPS: r.conf.Bool("http.tls"),
Cert: crt,
@@ -142,40 +252,23 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
}, nil
}
func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.PanelSetting) (bool, error) {
if err := r.Set(biz.SettingKeyName, setting.Name); err != nil {
func (r *settingRepo) UpdatePanel(req *request.SettingPanel) (bool, error) {
if err := r.Set(biz.SettingKeyName, req.Name); err != nil {
return false, err
}
if err := r.Set(biz.SettingKeyOfflineMode, cast.ToString(setting.OfflineMode)); err != nil {
if err := r.Set(biz.SettingKeyChannel, req.Channel); err != nil {
return false, err
}
if err := r.Set(biz.SettingKeyAutoUpdate, cast.ToString(setting.AutoUpdate)); err != nil {
if err := r.Set(biz.SettingKeyOfflineMode, cast.ToString(req.OfflineMode)); err != nil {
return false, err
}
if err := r.Set(biz.SettingKeyWebsitePath, setting.WebsitePath); err != nil {
if err := r.Set(biz.SettingKeyAutoUpdate, cast.ToString(req.AutoUpdate)); err != nil {
return false, err
}
if err := r.Set(biz.SettingKeyBackupPath, setting.BackupPath); err != nil {
if err := r.Set(biz.SettingKeyWebsitePath, req.WebsitePath); err != nil {
return false, err
}
// 用户
user := new(biz.User)
userID := cast.ToUint(ctx.Value("user_id"))
if err := r.db.Where("id = ?", userID).First(user).Error; err != nil {
return false, err
}
user.Username = setting.Username
user.Email = setting.Email
if setting.Password != "" {
value, err := hash.NewArgon2id().Make(setting.Password)
if err != nil {
return false, err
}
user.Password = value
}
if err := r.db.Save(user).Error; err != nil {
if err := r.Set(biz.SettingKeyBackupPath, req.BackupPath); err != nil {
return false, err
}
@@ -184,22 +277,22 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
restartFlag := false
oldCert, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
oldKey, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.key"))
if oldCert != setting.Cert || oldKey != setting.Key {
if oldCert != req.Cert || oldKey != req.Key {
if r.task.HasRunningTask() {
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
return false, errors.New(r.t.Get("background task is running, modifying some settings is prohibited, please try again later"))
}
restartFlag = true
}
if _, err := cert.ParseCert(setting.Cert); err != nil {
return false, fmt.Errorf("failed to parse certificate: %w", err)
if _, err := cert.ParseCert(req.Cert); err != nil {
return false, errors.New(r.t.Get("failed to parse certificate: %v", err))
}
if _, err := cert.ParseKey(setting.Key); err != nil {
return false, fmt.Errorf("failed to parse private key: %w", err)
if _, err := cert.ParseKey(req.Key); err != nil {
return false, errors.New(r.t.Get("failed to parse private key: %v", err))
}
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), setting.Cert, 0644); err != nil {
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), req.Cert, 0644); err != nil {
return false, err
}
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), setting.Key, 0644); err != nil {
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), req.Key, 0644); err != nil {
return false, err
}
@@ -213,16 +306,20 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
return false, err
}
if setting.Port != config.HTTP.Port {
if os.TCPPortInUse(setting.Port) {
return false, errors.New("端口已被占用")
if req.Port != config.HTTP.Port {
if os.TCPPortInUse(req.Port) {
return false, errors.New(r.t.Get("port is already in use"))
}
}
config.App.Locale = setting.Locale
config.HTTP.Port = setting.Port
config.HTTP.Entrance = setting.Entrance
config.HTTP.TLS = setting.HTTPS
config.App.Locale = req.Locale
config.HTTP.Port = req.Port
config.HTTP.Entrance = req.Entrance
config.HTTP.TLS = req.HTTPS
config.HTTP.BindDomain = req.BindDomain
config.HTTP.BindIP = req.BindIP
config.HTTP.BindUA = req.BindUA
config.Session.Lifetime = req.Lifetime
// 放行端口
fw := firewall.NewFirewall()
@@ -243,7 +340,7 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
}
if raw != string(encoded) {
if r.task.HasRunningTask() {
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
return false, errors.New(r.t.Get("background task is running, modifying some settings is prohibited, please try again later"))
}
restartFlag = true
}
@@ -253,3 +350,24 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
return restartFlag, nil
}
func (r *settingRepo) UpdateCert(req *request.SettingCert) error {
if r.task.HasRunningTask() {
return errors.New(r.t.Get("background task is running, modifying some settings is prohibited, please try again later"))
}
if _, err := cert.ParseCert(req.Cert); err != nil {
return errors.New(r.t.Get("failed to parse certificate: %v", err))
}
if _, err := cert.ParseKey(req.Key); err != nil {
return errors.New(r.t.Get("failed to parse private key: %v", err))
}
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), req.Cert, 0644); err != nil {
return err
}
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), req.Key, 0644); err != nil {
return err
}
return nil
}
+8 -4
View File
@@ -1,8 +1,10 @@
package data
import (
"errors"
"fmt"
"github.com/leonelquinteros/gotext"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/biz"
@@ -11,17 +13,19 @@ import (
)
type sshRepo struct {
t *gotext.Locale
db *gorm.DB
}
func NewSSHRepo(db *gorm.DB) biz.SSHRepo {
func NewSSHRepo(t *gotext.Locale, db *gorm.DB) biz.SSHRepo {
return &sshRepo{
t: t,
db: db,
}
}
func (r *sshRepo) List(page, limit uint) ([]*biz.SSH, int64, error) {
var ssh []*biz.SSH
ssh := make([]*biz.SSH, 0)
var total int64
err := r.db.Model(&biz.SSH{}).Omit("Hosts").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&ssh).Error
return ssh, total, err
@@ -46,7 +50,7 @@ func (r *sshRepo) Create(req *request.SSHCreate) error {
}
_, err := pkgssh.NewSSHClient(conf)
if err != nil {
return fmt.Errorf("failed to check ssh connection: %v", err)
return errors.New(r.t.Get("failed to check ssh connection: %v", err))
}
ssh := &biz.SSH{
@@ -70,7 +74,7 @@ func (r *sshRepo) Update(req *request.SSHUpdate) error {
}
_, err := pkgssh.NewSSHClient(conf)
if err != nil {
return fmt.Errorf("failed to check ssh connection: %v", err)
return errors.New(r.t.Get("failed to check ssh connection: %v", err))
}
ssh := &biz.SSH{
+8 -4
View File
@@ -1,9 +1,10 @@
package data
import (
"fmt"
"errors"
"log/slog"
"github.com/leonelquinteros/gotext"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/biz"
@@ -12,13 +13,15 @@ import (
)
type taskRepo struct {
t *gotext.Locale
db *gorm.DB
log *slog.Logger
queue *queue.Queue
}
func NewTaskRepo(db *gorm.DB, log *slog.Logger, queue *queue.Queue) biz.TaskRepo {
func NewTaskRepo(t *gotext.Locale, db *gorm.DB, log *slog.Logger, queue *queue.Queue) biz.TaskRepo {
return &taskRepo{
t: t,
db: db,
log: log,
queue: queue,
@@ -32,7 +35,7 @@ func (r *taskRepo) HasRunningTask() bool {
}
func (r *taskRepo) List(page, limit uint) ([]*biz.Task, int64, error) {
var tasks []*biz.Task
tasks := make([]*biz.Task, 0)
var total int64
err := r.db.Model(&biz.Task{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&tasks).Error
return tasks, total, err
@@ -53,12 +56,13 @@ func (r *taskRepo) UpdateStatus(id uint, status biz.TaskStatus) error {
}
func (r *taskRepo) Push(task *biz.Task) error {
// 防止有人喜欢酒吧点炒饭
var count int64
if err := r.db.Model(&biz.Task{}).Where("shell = ? and (status = ? or status = ?)", task.Shell, biz.TaskStatusWaiting, biz.TaskStatusRunning).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return fmt.Errorf("duplicate submission, please wait for the previous task to end")
return errors.New(r.t.Get("duplicate submission, please wait for the previous task to end"))
}
if err := r.db.Create(task).Error; err != nil {
+167 -34
View File
@@ -2,57 +2,38 @@ package data
import (
"errors"
"image"
"time"
"github.com/go-rat/utils/hash"
"github.com/leonelquinteros/gotext"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"github.com/spf13/cast"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/biz"
)
type userRepo struct {
t *gotext.Locale
db *gorm.DB
hasher hash.Hasher
}
func NewUserRepo(db *gorm.DB) biz.UserRepo {
func NewUserRepo(t *gotext.Locale, db *gorm.DB) biz.UserRepo {
return &userRepo{
t: t,
db: db,
hasher: hash.NewArgon2id(),
}
}
func (r *userRepo) Create(username, password string) (*biz.User, error) {
value, err := r.hasher.Make(password)
if err != nil {
return nil, err
}
user := &biz.User{
Username: username,
Password: value,
}
if err = r.db.Create(user).Error; err != nil {
return nil, err
}
return user, nil
}
func (r *userRepo) CheckPassword(username, password string) (*biz.User, error) {
user := new(biz.User)
if err := r.db.Where("username = ?", username).First(user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户名或密码错误")
} else {
return nil, err
}
}
if !r.hasher.Check(password, user.Password) {
return nil, errors.New("用户名或密码错误")
}
return user, nil
func (r *userRepo) List(page, limit uint) ([]*biz.User, int64, error) {
users := make([]*biz.User, 0)
var total int64
err := r.db.Model(&biz.User{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&users).Error
return users, total, err
}
func (r *userRepo) Get(id uint) (*biz.User, error) {
@@ -64,6 +45,158 @@ func (r *userRepo) Get(id uint) (*biz.User, error) {
return user, nil
}
func (r *userRepo) Save(user *biz.User) error {
func (r *userRepo) Create(username, password, email string) (*biz.User, error) {
value, err := r.hasher.Make(password)
if err != nil {
return nil, err
}
user := &biz.User{
Username: username,
Password: value,
Email: email,
}
if err = r.db.Create(user).Error; err != nil {
return nil, err
}
return user, nil
}
func (r *userRepo) UpdatePassword(id uint, password string) error {
value, err := r.hasher.Make(password)
if err != nil {
return err
}
user, err := r.Get(id)
if err != nil {
return err
}
user.Password = value
return r.db.Save(user).Error
}
func (r *userRepo) UpdateEmail(id uint, email string) error {
user, err := r.Get(id)
if err != nil {
return err
}
user.Email = email
return r.db.Save(user).Error
}
func (r *userRepo) Delete(id uint) error {
var count int64
if err := r.db.Model(&biz.User{}).Count(&count).Error; err != nil {
return err
}
if count <= 1 {
return errors.New(r.t.Get("please don't do this"))
}
user := new(biz.User)
if err := r.db.Preload("Tokens").First(user, id).Error; err != nil {
return err
}
return r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&user).Association("Tokens").Delete(); err != nil {
return err
}
return tx.Delete(&user).Error
})
}
func (r *userRepo) CheckPassword(username, password string) (*biz.User, error) {
user := new(biz.User)
if err := r.db.Where("username = ?", username).First(user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New(r.t.Get("username or password error"))
} else {
return nil, err
}
}
if !r.hasher.Check(password, user.Password) {
return nil, errors.New(r.t.Get("username or password error"))
}
return user, nil
}
func (r *userRepo) IsTwoFA(username string) (bool, error) {
user := new(biz.User)
if err := r.db.Where("username = ?", username).First(user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return false, errors.New(r.t.Get("username or password error"))
} else {
return false, err
}
}
return user.TwoFA != "", nil
}
func (r *userRepo) GenerateTwoFA(id uint) (image.Image, string, string, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: "RatPanel",
AccountName: cast.ToString(id),
SecretSize: 32,
Algorithm: otp.AlgorithmSHA256,
})
if err != nil {
return nil, "", "", err
}
img, err := key.Image(200, 200)
if err != nil {
return nil, "", "", err
}
return img, key.URL(), key.Secret(), nil
}
func (r *userRepo) UpdateTwoFA(id uint, code, secret string) error {
user, err := r.Get(id)
if err != nil {
return err
}
// 保存前先验证一次,防止错误开启
if secret != "" {
if valid, _ := totp.ValidateCustom(code, secret, time.Now().UTC(), totp.ValidateOpts{
Skew: 1,
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA256,
}); !valid {
return errors.New(r.t.Get("invalid 2FA code"))
}
}
user.TwoFA = secret
return r.db.Save(user).Error
}
func (r *userRepo) CheckTwoFA(id uint, code string) (bool, error) {
user, err := r.Get(id)
if err != nil {
return false, err
}
if user.TwoFA == "" {
return true, nil // 未开启2FA,无需验证
}
if valid, _ := totp.ValidateCustom(code, user.TwoFA, time.Now().UTC(), totp.ValidateOpts{
Skew: 1,
Digits: otp.DigitsSix,
Algorithm: otp.AlgorithmSHA256,
}); !valid {
return false, errors.New(r.t.Get("invalid 2FA code"))
}
return true, nil
}
+159
View File
@@ -0,0 +1,159 @@
package data
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"net/http"
"slices"
"time"
"github.com/go-rat/utils/str"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/biz"
)
type userTokenRepo struct {
t *gotext.Locale
db *gorm.DB
}
func NewUserTokenRepo(t *gotext.Locale, db *gorm.DB) biz.UserTokenRepo {
return &userTokenRepo{
t: t,
db: db,
}
}
func (r userTokenRepo) List(userID, page, limit uint) ([]*biz.UserToken, int64, error) {
userTokens := make([]*biz.UserToken, 0)
var total int64
err := r.db.Model(&biz.UserToken{}).Where("user_id = ?", userID).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&userTokens).Error
return userTokens, total, err
}
func (r userTokenRepo) Create(userID uint, ips []string, expired time.Time) (*biz.UserToken, error) {
token := str.Random(32)
userToken := &biz.UserToken{
UserID: userID,
Token: token,
IPs: ips,
ExpiredAt: expired,
}
if err := r.db.Create(userToken).Error; err != nil {
return nil, err
}
userToken.Token = token // 返回的值是加密的,这里覆盖为原始值
return userToken, nil
}
func (r userTokenRepo) Get(id uint) (*biz.UserToken, error) {
userToken := new(biz.UserToken)
if err := r.db.First(userToken, id).Error; err != nil {
return nil, err
}
return userToken, nil
}
func (r userTokenRepo) Delete(id uint) error {
userToken := new(biz.UserToken)
if err := r.db.First(userToken, id).Error; err != nil {
return err
}
return r.db.Delete(userToken).Error
}
func (r userTokenRepo) Update(id uint, ips []string, expired time.Time) (*biz.UserToken, error) {
userToken := new(biz.UserToken)
if err := r.db.First(userToken, id).Error; err != nil {
return nil, err
}
userToken.IPs = ips
userToken.ExpiredAt = expired
if err := r.db.Save(userToken).Error; err != nil {
return nil, err
}
return userToken, nil
}
func (r userTokenRepo) ValidateReq(req *http.Request) (uint, error) {
// Authorization: HMAC-SHA256 Credential=<token_id>, Signature=<signature>
var algorithm string
var id uint
var signature string
if _, err := fmt.Sscanf(req.Header.Get("Authorization"), "%s Credential=%d, Signature=%s", &algorithm, &id, &signature); err != nil {
return 0, errors.New(r.t.Get("invalid header: %v", err))
}
if algorithm != "HMAC-SHA256" {
return 0, errors.New(r.t.Get("invalid signature"))
}
// 获取用户令牌
userToken, err := r.Get(id)
if err != nil {
return 0, errors.New(r.t.Get("invalid signature")) // 不应返回原始报错,防止猜测令牌ID
}
if userToken.ExpiredAt.Before(time.Now()) {
return 0, errors.New(r.t.Get("token expired"))
}
// 步骤一:构造规范化请求
body, err := io.ReadAll(req.Body)
if err != nil {
return 0, err
}
req.Body = io.NopCloser(bytes.NewReader(body))
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s", req.Method, req.URL.Path, req.URL.Query().Encode(), str.SHA256(string(body)))
// 步骤二:构造待签名字符串
timestamp := cast.ToInt64(req.Header.Get("X-Timestamp"))
stringToSign := fmt.Sprintf("%s\n%d\n%s", "HMAC-SHA256", cast.ToInt64(timestamp), str.SHA256(canonicalRequest))
// 步骤三:计算签名
validSignature := r.hmacsha256(stringToSign, userToken.Token)
// 步骤四:验证签名
if subtle.ConstantTimeCompare([]byte(signature), []byte(validSignature)) != 1 {
return 0, errors.New(r.t.Get("invalid signature"))
}
// 步骤五:验证时间戳
if timestamp == 0 || timestamp < (time.Now().Unix()-300) {
return 0, errors.New(r.t.Get("signature expired"))
}
// 步骤六:验证IP
if len(userToken.IPs) > 0 {
ip, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
ip = req.RemoteAddr
}
if !slices.Contains(userToken.IPs, ip) {
return 0, errors.New(r.t.Get("invalid request ip: %s", ip))
}
}
return userToken.UserID, nil
}
func (r userTokenRepo) hmacsha256(data string, secret string) string {
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
+95 -33
View File
@@ -11,17 +11,18 @@ import (
"strings"
"time"
"github.com/leonelquinteros/gotext"
"github.com/samber/lo"
"github.com/spf13/cast"
"gorm.io/gorm"
"github.com/tnb-labs/panel/internal/app"
"github.com/tnb-labs/panel/internal/biz"
"github.com/tnb-labs/panel/internal/embed"
"github.com/tnb-labs/panel/internal/http/request"
"github.com/tnb-labs/panel/pkg/acme"
"github.com/tnb-labs/panel/pkg/api"
"github.com/tnb-labs/panel/pkg/cert"
"github.com/tnb-labs/panel/pkg/embed"
"github.com/tnb-labs/panel/pkg/io"
"github.com/tnb-labs/panel/pkg/nginx"
"github.com/tnb-labs/panel/pkg/punycode"
@@ -31,6 +32,7 @@ import (
)
type websiteRepo struct {
t *gotext.Locale
db *gorm.DB
cache biz.CacheRepo
database biz.DatabaseRepo
@@ -40,8 +42,9 @@ type websiteRepo struct {
certAccount biz.CertAccountRepo
}
func NewWebsiteRepo(db *gorm.DB, cache biz.CacheRepo, database biz.DatabaseRepo, databaseServer biz.DatabaseServerRepo, databaseUser biz.DatabaseUserRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo) biz.WebsiteRepo {
func NewWebsiteRepo(t *gotext.Locale, db *gorm.DB, cache biz.CacheRepo, database biz.DatabaseRepo, databaseServer biz.DatabaseServerRepo, databaseUser biz.DatabaseUserRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo) biz.WebsiteRepo {
return &websiteRepo{
t: t,
db: db,
cache: cache,
database: database,
@@ -109,6 +112,7 @@ func (r *websiteRepo) Get(id uint) (*types.WebsiteSetting, error) {
setting := new(types.WebsiteSetting)
setting.ID = website.ID
setting.Name = website.Name
setting.Type = website.Type
setting.Path = website.Path
setting.HTTPS = website.Https
setting.PHP = p.GetPHP()
@@ -208,7 +212,7 @@ func (r *websiteRepo) GetByName(name string) (*types.WebsiteSetting, error) {
}
func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) {
var websites []*biz.Website
websites := make([]*biz.Website, 0)
var total int64
if err := r.db.Model(&biz.Website{}).Count(&total).Error; err != nil {
@@ -224,7 +228,11 @@ func (r *websiteRepo) List(page, limit uint) ([]*biz.Website, int64, error) {
func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
// 初始化nginx配置
p, err := nginx.NewParser()
config := nginx.DefaultConf
if app.Locale == "zh_CN" {
config = nginx.DefaultConfZh
}
p, err := nginx.NewParser(config)
if err != nil {
return nil, err
}
@@ -259,7 +267,7 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
}
includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", req.Name+".conf"))
includes = append(includes, filepath.Join(app.Root, "server/vhost/acme", req.Name+".conf"))
comments = append(comments, []string{"# 伪静态规则"})
comments = append(comments, []string{r.t.Get("# Rewrite rule")})
comments = append(comments, []string{"# acme http-01"})
if err = p.SetIncludes(includes, comments); err != nil {
return nil, err
@@ -276,16 +284,26 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
if err = os.MkdirAll(req.Path, 0755); err != nil {
return nil, err
}
index, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html"))
var index []byte
if app.Locale == "zh_CN" {
index, err = embed.WebsiteFS.ReadFile(filepath.Join("website", "index_zh.html"))
} else {
index, err = embed.WebsiteFS.ReadFile(filepath.Join("website", "index.html"))
}
if err != nil {
return nil, fmt.Errorf("获取index模板文件失败: %w", err)
return nil, errors.New(r.t.Get("failed to get index template file: %v", err))
}
if err = io.Write(filepath.Join(req.Path, "index.html"), string(index), 0644); err != nil {
return nil, err
}
notFound, err := embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html"))
var notFound []byte
if app.Locale == "zh_CN" {
notFound, err = embed.WebsiteFS.ReadFile(filepath.Join("website", "404_zh.html"))
} else {
notFound, err = embed.WebsiteFS.ReadFile(filepath.Join("website", "404.html"))
}
if err != nil {
return nil, fmt.Errorf("获取404模板文件失败: %w", err)
return nil, errors.New(r.t.Get("failed to get 404 template file: %v", err))
}
if err = io.Write(filepath.Join(req.Path, "404.html"), string(notFound), 0644); err != nil {
return nil, err
@@ -330,6 +348,7 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
// 创建面板网站
w := &biz.Website{
Name: req.Name,
Type: "php", // TODO 支持网站类型
Status: true,
Path: req.Path,
Https: false,
@@ -349,7 +368,7 @@ func (r *websiteRepo) Create(req *request.WebsiteCreate) (*biz.Website, error) {
if req.DB {
server, err := r.databaseServer.GetByName(name)
if err != nil {
return nil, fmt.Errorf(`create database: can't find %s database server, please add it first`, name)
return nil, errors.New(r.t.Get("can't find %s database server, please add it first", name))
}
if err = r.database.Create(&request.DatabaseCreate{
ServerID: server.ID,
@@ -372,9 +391,6 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
if err := r.db.Where("id", req.ID).First(website).Error; err != nil {
return err
}
if !website.Status {
return errors.New("网站已停用,请先启用")
}
// 解析nginx配置
config, err := io.Read(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
@@ -430,14 +446,14 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
}
// 运行目录
if !io.Exists(req.Root) {
return errors.New("运行目录不存在")
return errors.New(r.t.Get("runtime directory does not exist"))
}
if err = p.SetRoot(req.Root); err != nil {
return err
}
// 运行目录
if !io.Exists(req.Path) {
return errors.New("网站目录不存在")
return errors.New(r.t.Get("website directory does not exist"))
}
website.Path = req.Path
// PHP
@@ -455,6 +471,12 @@ func (r *websiteRepo) Update(req *request.WebsiteUpdate) error {
}
website.Https = req.HTTPS
if req.HTTPS {
if _, err = cert.ParseCert(req.SSLCertificate); err != nil {
return errors.New(r.t.Get("failed to parse certificate: %v", err))
}
if _, err = cert.ParseKey(req.SSLCertificateKey); err != nil {
return errors.New(r.t.Get("failed to parse private key: %v", err))
}
if err = p.SetHTTPS(certPath, keyPath); err != nil {
return err
}
@@ -535,7 +557,7 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
return err
}
if website.Cert != nil {
return errors.New("网站" + website.Name + "已绑定证书,请先删除证书")
return errors.New(r.t.Get("website %s has bound certificates, please delete the certificate first", website.Name))
}
_ = io.Remove(filepath.Join(app.Root, "server/vhost", website.Name+".conf"))
@@ -578,7 +600,7 @@ func (r *websiteRepo) ClearLog(id uint) error {
return err
}
_, err := shell.Execf(`echo "" > %s/wwwlogs/%s.log`, app.Root, website.Name)
_, err := shell.Execf(`cat /dev/null > %s/wwwlogs/%s.log`, app.Root, website.Name)
return err
}
@@ -599,7 +621,11 @@ func (r *websiteRepo) ResetConfig(id uint) error {
}
// 初始化nginx配置
p, err := nginx.NewParser()
config := nginx.DefaultConf
if app.Locale == "zh_CN" {
config = nginx.DefaultConfZh
}
p, err := nginx.NewParser(config)
if err != nil {
return err
}
@@ -614,7 +640,7 @@ func (r *websiteRepo) ResetConfig(id uint) error {
}
includes = append(includes, filepath.Join(app.Root, "server/vhost/rewrite", website.Name+".conf"))
includes = append(includes, filepath.Join(app.Root, "server/vhost/acme", website.Name+".conf"))
comments = append(comments, []string{"# 伪静态规则"})
comments = append(comments, []string{r.t.Get("# Rewrite rule")})
comments = append(comments, []string{"# acme http-01"})
if err = p.SetIncludes(includes, comments); err != nil {
return err
@@ -680,23 +706,23 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error {
if status {
if len(rootComment) == 0 {
return fmt.Errorf("未找到运行目录注释")
return errors.New(r.t.Get("runtime directory comment not found"))
}
if len(rootComment) != 1 {
return fmt.Errorf("运行目录注释数量不正确,预期1个,实际%d", len(rootComment))
return errors.New(r.t.Get("runtime directory comment count is incorrect, expected 1, actual %d", len(rootComment)))
}
rootComment[0] = strings.TrimPrefix(rootComment[0], "# ")
if !io.Exists(rootComment[0]) {
return fmt.Errorf("运行目录不存在")
return errors.New(r.t.Get("runtime directory does not exist"))
}
if err = p.SetRoot(rootComment[0]); err != nil {
return err
}
if len(indexComment) == 0 {
return fmt.Errorf("未找到默认文档注释")
return errors.New(r.t.Get("default document comment not found"))
}
if len(indexComment) != 1 {
return fmt.Errorf("默认文档注释数量不正确,预期1个,实际%d", len(indexComment))
return errors.New(r.t.Get("default document comment count is incorrect, expected 1, actual %d", len(indexComment)))
}
indexComment[0] = strings.TrimPrefix(indexComment[0], "# ")
if err = p.SetIndex(strings.Fields(indexComment[0])); err != nil {
@@ -728,13 +754,45 @@ func (r *websiteRepo) UpdateStatus(id uint, status bool) error {
return nil
}
func (r *websiteRepo) UpdateCert(req *request.WebsiteUpdateCert) error {
website := new(biz.Website)
if err := r.db.Where("name", req.Name).First(&website).Error; err != nil {
return err
}
if _, err := cert.ParseCert(req.Cert); err != nil {
return errors.New(r.t.Get("failed to parse certificate: %v", err))
}
if _, err := cert.ParseKey(req.Key); err != nil {
return errors.New(r.t.Get("failed to parse private key: %v", err))
}
certPath := filepath.Join(app.Root, "server/vhost/cert", website.Name+".pem")
keyPath := filepath.Join(app.Root, "server/vhost/cert", website.Name+".key")
if err := io.Write(certPath, req.Cert, 0644); err != nil {
return err
}
if err := io.Write(keyPath, req.Key, 0644); err != nil {
return err
}
if website.Https {
if err := systemctl.Reload("nginx"); err != nil {
_, err = shell.Execf("nginx -t")
return err
}
}
return nil
}
func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
website, err := r.Get(id)
if err != nil {
return err
}
if slices.Contains(website.Domains, "*") {
return errors.New("cannot one-key obtain wildcard certificate")
return errors.New(r.t.Get("not support one-key obtain wildcard certificate, please use Cert menu to obtain it with DNS method"))
}
account, err := r.certAccount.GetDefault(cast.ToUint(ctx.Value("user_id")))
@@ -744,14 +802,18 @@ func (r *websiteRepo) ObtainCert(ctx context.Context, id uint) error {
newCert, err := r.cert.GetByWebsite(website.ID)
if err != nil {
newCert, err = r.cert.Create(&request.CertCreate{
Type: string(acme.KeyEC256),
Domains: website.Domains,
AutoRenew: true,
AccountID: account.ID,
WebsiteID: website.ID,
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
newCert, err = r.cert.Create(&request.CertCreate{
Type: string(acme.KeyEC256),
Domains: website.Domains,
AutoRenew: true,
AccountID: account.ID,
WebsiteID: website.ID,
})
if err != nil {
return err
}
} else {
return err
}
}
+75 -5
View File
@@ -1,17 +1,23 @@
package middleware
import (
"net"
"net/http"
"slices"
"strings"
"github.com/go-chi/chi/v5"
"github.com/go-rat/chix"
"github.com/go-rat/sessions"
"github.com/knadh/koanf/v2"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/pkg/punycode"
)
// Entrance 确保通过正确的入口访问
func Entrance(conf *koanf.Koanf, session *sessions.Manager) func(next http.Handler) http.Handler {
func Entrance(t *gotext.Locale, conf *koanf.Koanf, session *sessions.Manager) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sess, err := session.GetSession(r)
@@ -20,13 +26,63 @@ func Entrance(conf *koanf.Koanf, session *sessions.Manager) func(next http.Handl
defer render.Release()
render.Status(http.StatusInternalServerError)
render.JSON(chix.M{
"message": err.Error(),
"msg": err.Error(),
})
return
}
entrance := conf.String("http.entrance")
if strings.TrimSuffix(r.URL.Path, "/") == strings.TrimSuffix(entrance, "/") {
entrance := strings.TrimSuffix(conf.String("http.entrance"), "/")
if entrance == "" {
entrance = "/"
}
if !strings.HasPrefix(entrance, "/") {
entrance = "/" + entrance
}
// 情况一:设置了绑定域名、IP、UA,且请求不符合要求,返回错误
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
host = r.Host
}
if strings.Contains(host, "xn--") {
if decoded, err := punycode.DecodeDomain(host); err == nil {
host = decoded
}
}
if len(conf.Strings("http.bind_domain")) > 0 && !slices.Contains(conf.Strings("http.bind_domain"), host) {
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusTeapot)
render.JSON(chix.M{
"msg": t.Get("invalid request domain: %s", r.Host),
})
return
}
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
ip = r.RemoteAddr
}
if len(conf.Strings("http.bind_ip")) > 0 && !slices.Contains(conf.Strings("http.bind_ip"), ip) {
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusTeapot)
render.JSON(chix.M{
"msg": t.Get("invalid request ip: %s", ip),
})
return
}
if len(conf.Strings("http.bind_ua")) > 0 && !slices.Contains(conf.Strings("http.bind_ua"), r.UserAgent()) {
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusTeapot)
render.JSON(chix.M{
"msg": t.Get("invalid request user agent: %s", r.UserAgent()),
})
return
}
// 情况二:请求路径与入口路径相同,标记通过验证并重定向到登录页面
if strings.TrimSuffix(r.URL.Path, "/") == entrance {
sess.Put("verify_entrance", true)
render := chix.NewRender(w, r)
defer render.Release()
@@ -34,6 +90,20 @@ func Entrance(conf *koanf.Koanf, session *sessions.Manager) func(next http.Handl
return
}
// 情况三:通过APIKey+入口路径访问,重写请求路径并跳过验证
if strings.HasPrefix(r.URL.Path, entrance) && r.Header.Get("Authorization") != "" {
// 只在设置了入口路径的情况下,才进行重写
if entrance != "/" {
if rctx := chi.RouteContext(r.Context()); rctx != nil {
rctx.RoutePath = strings.TrimPrefix(rctx.RoutePath, entrance)
}
r.URL.Path = strings.TrimPrefix(r.URL.Path, entrance)
}
next.ServeHTTP(w, r)
return
}
// 情况三:非调试模式且未通过验证的请求,返回错误
if !conf.Bool("app.debug") &&
!cast.ToBool(sess.Get("verify_entrance", false)) &&
r.URL.Path != "/robots.txt" {
@@ -41,7 +111,7 @@ func Entrance(conf *koanf.Koanf, session *sessions.Manager) func(next http.Handl
defer render.Release()
render.Status(http.StatusTeapot)
render.JSON(chix.M{
"message": "请通过正确的入口访问",
"msg": t.Get("invalid access entrance"),
})
return
}
+17 -14
View File
@@ -11,6 +11,7 @@ import (
"github.com/golang-cz/httplog"
"github.com/google/wire"
"github.com/knadh/koanf/v2"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/biz"
)
@@ -18,23 +19,25 @@ import (
var ProviderSet = wire.NewSet(NewMiddlewares)
type Middlewares struct {
conf *koanf.Koanf
log *slog.Logger
session *sessions.Manager
app biz.AppRepo
conf *koanf.Koanf
log *slog.Logger
session *sessions.Manager
app biz.AppRepo
userToken biz.UserTokenRepo
}
func NewMiddlewares(conf *koanf.Koanf, log *slog.Logger, session *sessions.Manager, app biz.AppRepo) *Middlewares {
func NewMiddlewares(conf *koanf.Koanf, log *slog.Logger, session *sessions.Manager, app biz.AppRepo, userToken biz.UserTokenRepo) *Middlewares {
return &Middlewares{
conf: conf,
log: log,
session: session,
app: app,
conf: conf,
log: log,
session: session,
app: app,
userToken: userToken,
}
}
// Globals is a collection of global middleware that will be applied to every request.
func (r *Middlewares) Globals(mux *chi.Mux) []func(http.Handler) http.Handler {
func (r *Middlewares) Globals(t *gotext.Locale, mux *chi.Mux) []func(http.Handler) http.Handler {
return []func(http.Handler) http.Handler{
sessionmiddleware.StartSession(r.session),
//middleware.SupressNotFound(mux),// bug https://github.com/go-chi/chi/pull/940
@@ -46,9 +49,9 @@ func (r *Middlewares) Globals(mux *chi.Mux) []func(http.Handler) http.Handler {
LogRequestHeaders: []string{"User-Agent"},
}),
middleware.Recoverer,
Status,
Entrance(r.conf, r.session),
MustLogin(r.session),
MustInstall(r.app),
Status(t),
Entrance(t, r.conf, r.session),
MustLogin(t, r.session, r.userToken),
MustInstall(t, r.app),
}
}
+4 -4
View File
@@ -1,17 +1,17 @@
package middleware
import (
"fmt"
"net/http"
"strings"
"github.com/go-rat/chix"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/biz"
)
// MustInstall 确保已安装应用
func MustInstall(app biz.AppRepo) func(next http.Handler) http.Handler {
func MustInstall(t *gotext.Locale, app biz.AppRepo) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var slugs []string
@@ -26,7 +26,7 @@ func MustInstall(app biz.AppRepo) func(next http.Handler) http.Handler {
defer render.Release()
render.Status(http.StatusForbidden)
render.JSON(chix.M{
"message": "应用不存在",
"msg": t.Get("app not found"),
})
return
}
@@ -45,7 +45,7 @@ func MustInstall(app biz.AppRepo) func(next http.Handler) http.Handler {
defer render.Release()
render.Status(http.StatusForbidden)
render.JSON(chix.M{
"message": fmt.Sprintf("应用 %s 未安装", slugs),
"msg": t.Get("app %s not installed", slugs),
})
return
}
+58 -29
View File
@@ -11,17 +11,21 @@ import (
"github.com/go-rat/chix"
"github.com/go-rat/sessions"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cast"
"github.com/tnb-labs/panel/internal/biz"
)
// MustLogin 确保已登录
func MustLogin(session *sessions.Manager) func(next http.Handler) http.Handler {
func MustLogin(t *gotext.Locale, session *sessions.Manager, userToken biz.UserTokenRepo) func(next http.Handler) http.Handler {
// 白名单
whiteList := []string{
"/api/user/key",
"/api/user/login",
"/api/user/logout",
"/api/user/isLogin",
"/api/user/is_login",
"/api/user/is_2fa",
"/api/dashboard/panel",
}
return func(next http.Handler) http.Handler {
@@ -32,7 +36,7 @@ func MustLogin(session *sessions.Manager) func(next http.Handler) http.Handler {
defer render.Release()
render.Status(http.StatusInternalServerError)
render.JSON(chix.M{
"message": err.Error(),
"msg": err.Error(),
})
return
}
@@ -43,43 +47,68 @@ func MustLogin(session *sessions.Manager) func(next http.Handler) http.Handler {
return
}
if sess.Missing("user_id") {
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusUnauthorized)
render.JSON(chix.M{
"message": "会话已过期,请重新登录",
})
return
userID := uint(0)
if r.Header.Get("Authorization") != "" {
// 禁止访问 ws 相关的接口
if strings.HasPrefix(r.URL.Path, "/api/ws") {
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusForbidden)
render.JSON(chix.M{
"msg": t.Get("ws not allowed"),
})
return
}
// API 请求验证
if userID, err = userToken.ValidateReq(r); err != nil {
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusUnauthorized)
render.JSON(chix.M{
"msg": err.Error(),
})
return
}
} else {
if sess.Missing("user_id") {
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusUnauthorized)
render.JSON(chix.M{
"msg": t.Get("session expired, please login again"),
})
return
}
safeLogin := cast.ToBool(sess.Get("safe_login"))
if safeLogin {
safeClientHash := cast.ToString(sess.Get("safe_client"))
ip, _, _ := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
clientHash := fmt.Sprintf("%x", sha256.Sum256([]byte(ip)))
if safeClientHash != clientHash || safeClientHash == "" {
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusUnauthorized)
render.JSON(chix.M{
"msg": t.Get("client ip/ua changed, please login again"),
})
return
}
}
userID = cast.ToUint(sess.Get("user_id"))
}
userID := cast.ToUint(sess.Get("user_id"))
if userID == 0 {
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusUnauthorized)
render.JSON(chix.M{
"message": "会话无效,请重新登录",
"msg": t.Get("invalid user id, please login again"),
})
return
}
safeLogin := cast.ToBool(sess.Get("safe_login"))
if safeLogin {
safeClientHash := cast.ToString(sess.Get("safe_client"))
ip, _, _ := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
clientHash := fmt.Sprintf("%x", sha256.Sum256([]byte(ip)))
if safeClientHash != clientHash || safeClientHash == "" {
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusUnauthorized)
render.JSON(chix.M{
"message": "客户端IP/UA变化,请重新登录",
})
return
}
}
r = r.WithContext(context.WithValue(r.Context(), "user_id", userID)) // nolint:staticcheck
next.ServeHTTP(w, r)
})
+42 -39
View File
@@ -4,48 +4,51 @@ import (
"net/http"
"github.com/go-rat/chix"
"github.com/leonelquinteros/gotext"
"github.com/tnb-labs/panel/internal/app"
)
// Status 检查程序状态
func Status(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch app.Status {
case app.StatusUpgrade:
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusServiceUnavailable)
render.JSON(chix.M{
"message": "面板更新中,请稍后刷新",
})
return
case app.StatusMaintain:
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusServiceUnavailable)
render.JSON(chix.M{
"message": "面板正在运行维护任务,请稍后刷新",
})
return
case app.StatusClosed:
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusForbidden)
render.JSON(chix.M{
"message": "面板已关闭",
})
return
case app.StatusFailed:
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusInternalServerError)
render.JSON(chix.M{
"message": "面板运行出错,请检查排除或联系支持",
})
return
default:
next.ServeHTTP(w, r)
}
})
func Status(t *gotext.Locale) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch app.Status {
case app.StatusUpgrade:
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusServiceUnavailable)
render.JSON(chix.M{
"msg": t.Get("panel is upgrading, please refresh later"),
})
return
case app.StatusMaintain:
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusServiceUnavailable)
render.JSON(chix.M{
"msg": t.Get("panel is maintaining, please refresh later"),
})
return
case app.StatusClosed:
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusForbidden)
render.JSON(chix.M{
"msg": t.Get("panel is closed"),
})
return
case app.StatusFailed:
render := chix.NewRender(w)
defer render.Release()
render.Status(http.StatusInternalServerError)
render.JSON(chix.M{
"msg": t.Get("panel run error, please check or contact support"),
})
return
default:
next.ServeHTTP(w, r)
}
})
}
}
+11 -4
View File
@@ -1,22 +1,29 @@
package request
import "mime/multipart"
type BackupList struct {
Type string `json:"type" form:"type" validate:"required|in:path,website,mysql,postgres,redis,panel"`
Type string `uri:"type" form:"type" validate:"required|in:path,website,mysql,postgres,redis,panel"`
}
type BackupCreate struct {
Type string `json:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"`
Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"`
Target string `json:"target" form:"target" validate:"required"`
Path string `json:"path" form:"path"`
}
type BackupUpload struct {
Type string `uri:"type" form:"type"` // 校验没有必要,因为根本没经过验证器
File *multipart.FileHeader `form:"file"`
}
type BackupFile struct {
Type string `json:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"`
Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"`
File string `json:"file" form:"file" validate:"required"`
}
type BackupRestore struct {
Type string `json:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"`
Type string `uri:"type" form:"type" validate:"required|in:website,mysql,postgres,redis,panel"`
File string `json:"file" form:"file" validate:"required"`
Target string `json:"target" form:"target" validate:"required"`
}
+2 -1
View File
@@ -16,10 +16,11 @@ type CertCreate struct {
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"`
Type string `form:"type" json:"type" validate:"required|in:P256,P384,2048,3072,4096,upload"`
Domains []string `form:"domains" json:"domains" validate:"required|isSlice"`
Cert string `form:"cert" json:"cert"`
Key string `form:"key" json:"key"`
Script string `form:"script" json:"script"`
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
AccountID uint `form:"account_id" json:"account_id"`
DNSID uint `form:"dns_id" json:"dns_id"`
+2 -2
View File
@@ -3,14 +3,14 @@ package request
import "github.com/tnb-labs/panel/pkg/acme"
type CertDNSCreate struct {
Type string `form:"type" json:"type" validate:"required"`
Type acme.DnsType `form:"type" json:"type" validate:"required|in:aliyun,tencent,huawei,westcn,cloudflare,godaddy,gcore,porkbun,namecheap,namesilo,namecom,cloudns,duckdns,hetzner,linode,vercel"`
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 string `form:"type" json:"type" validate:"required"`
Type acme.DnsType `form:"type" json:"type" validate:"required|in:aliyun,tencent,huawei,westcn,cloudflare,godaddy,gcore,porkbun,namecheap,namesilo,namecom,cloudns,duckdns,hetzner,linode,vercel"`
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" validate:"required|min:1"`
ID uint `json:"id" form:"id" query:"id" uri:"id" validate:"required|min:1"`
}
@@ -0,0 +1,32 @@
package request
import "github.com/tnb-labs/panel/pkg/types"
type ContainerComposeGet struct {
Name string `uri:"name" validate:"required"`
}
type ContainerComposeCreate struct {
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_-]+$"`
Compose string `json:"compose" validate:"required"`
Envs []types.KV `json:"envs"`
}
type ContainerComposeUp struct {
Name string `uri:"name" validate:"required"`
Force bool `json:"force"`
}
type ContainerComposeDown struct {
Name string `uri:"name" validate:"required"`
}
type ContainerComposeRemove struct {
Name string `uri:"name" validate:"required"`
}
+1 -1
View File
@@ -7,7 +7,7 @@ type CronCreate struct {
Script string `form:"script" json:"script"`
BackupType string `form:"backup_type" json:"backup_type" validate:"requiredIf:Type,backup"`
BackupPath string `form:"backup_path" json:"backup_path"`
Target string `form:"target" json:"target" validate:"required"`
Target string `form:"target" json:"target" validate:"requiredIf:Type,backup,cutoff"`
Save int `form:"save" json:"save" validate:"required"`
}
+33 -15
View File
@@ -1,18 +1,36 @@
package request
type PanelSetting struct {
Name string `json:"name" validate:"required"`
Locale string `json:"locale" validate:"required"`
Entrance string `json:"entrance" validate:"required"`
OfflineMode bool `json:"offline_mode"`
AutoUpdate bool `json:"auto_update"`
WebsitePath string `json:"website_path" validate:"required"`
BackupPath string `json:"backup_path" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"password"`
Email string `json:"email" validate:"required"`
Port uint `json:"port" validate:"required|min:1|max:65535"`
HTTPS bool `json:"https"`
Cert string `json:"cert" validate:"required"`
Key string `json:"key" validate:"required"`
import "net/http"
type SettingPanel struct {
Name string `json:"name" validate:"required"`
Channel string `json:"channel" validate:"required|in:stable,beta"`
Locale string `json:"locale" validate:"required"`
Entrance string `json:"entrance" validate:"required"`
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"` // 登录超时,单位:分
BindDomain []string `json:"bind_domain"`
BindIP []string `json:"bind_ip"`
BindUA []string `json:"bind_ua"`
WebsitePath string `json:"website_path" validate:"required"`
BackupPath string `json:"backup_path" validate:"required"`
Port uint `json:"port" validate:"required|min:1|max:65535"`
HTTPS bool `json:"https"`
Cert string `json:"cert" validate:"required"`
Key string `json:"key" validate:"required"`
}
func (r *SettingPanel) Rules(_ *http.Request) map[string]string {
return map[string]string{
"BindDomain.*": "required",
"BindIP.*": "required|ip",
"BindUA.*": "required",
}
}
type SettingCert struct {
Cert string `json:"cert" validate:"required"`
Key string `json:"key" validate:"required"`
}

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