Compare commits

...

332 Commits

Author SHA1 Message Date
耗子 d64f099f8e feat: 发布v2.3.18 2024-11-10 08:37:38 +08:00
耗子 294a78cd95 feat: 优化删除检查 2024-11-10 03:26:08 +08:00
renovate[bot] 8294a1a74e chore(deps): Update module github.com/urfave/cli/v3 to v3.0.0-alpha9.3 (#349)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-10 02:28:09 +08:00
renovate[bot] c403efd762 chore(deps): Update dependency @iconify/json to v2.2.270 (#348)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 18:02:04 +08:00
耗子 cf8dfca4c0 Merge remote-tracking branch 'origin/main' 2024-11-09 18:00:15 +08:00
耗子 e552caa38f feat: 修改ip获取地址 2024-11-09 18:00:09 +08:00
renovate[bot] ddff13990a chore(deps): Update dependency marked to v15 (#347)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 06:58:36 +00:00
renovate[bot] 27f79ea7a3 chore(deps): Update dependency pinia-plugin-persistedstate to v4.1.3 (#346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 01:32:12 +00:00
耗子 04d570197a feat: 添加redis驱动 2024-11-09 02:41:47 +08:00
renovate[bot] 8e74428023 chore(deps): Update all dependencies (#345)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-09 01:10:12 +08:00
renovate[bot] f4c09324b7 chore(deps): Update dependency md-editor-v3 to v4.21.3 (#344)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 11:08:41 +00:00
renovate[bot] ee6a12fac2 chore(deps): Update module golang.org/x/crypto to v0.29.0 (#343)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-08 03:10:34 +00:00
renovate[bot] ba429068ad chore(deps): Update dependency marked to v14.1.4 (#342)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-07 21:48:01 +00:00
renovate[bot] 711ccb4504 chore(deps): Update dependency @iconify/json to v2.2.269 (#341)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-07 19:09:13 +08:00
耗子 7d6e078e20 chore: 添加新合作 2024-11-07 16:58:49 +08:00
耗子 d3a7c9e29c feat: 数据库密码可以为空(本地认证) 2024-11-07 02:37:29 +08:00
耗子 10d22bfbc0 feat: 初始化数据库服务 2024-11-07 02:27:17 +08:00
耗子 724d05d980 feat: 优化数据库模型 2024-11-07 02:13:24 +08:00
耗子 690e7f0391 feat: 重命名安全为数据库 2024-11-07 01:12:38 +08:00
耗子 a28fc592d1 feat: 优化数据库模型 2024-11-07 00:52:12 +08:00
耗子 ff7e76ac2b feat: 初始化数据库条目模型 2024-11-07 00:49:11 +08:00
耗子 f2eb3a180c feat: 对部分密码进行加密 2024-11-07 00:25:09 +08:00
renovate[bot] 020a4ad870 chore(deps): Update all dependencies (#340)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-06 22:16:57 +08:00
renovate[bot] b5e7cfc069 chore(deps): Update all dependencies (#339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 21:31:06 +08:00
renovate[bot] 52f8a6f045 chore(deps): Update dependency @types/node to v22.9.0 (#338)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-05 05:13:50 +00:00
耗子 3831d47a23 feat(文件): 路径切换后清空已选择 2024-11-05 02:57:30 +08:00
耗子 3a579f2346 feat: 运行等待任务前标记先前运行中的任务为失败 2024-11-04 17:59:13 +08:00
耗子 e85e8dffb3 fix: lint 2024-11-04 17:48:46 +08:00
耗子 e18d40b0ab feat: 防止任务重复提交 2024-11-04 17:47:39 +08:00
renovate[bot] c308b8e42c chore(deps): Update all dependencies (#336)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 12:40:20 +08:00
renovate[bot] cafded526f chore(deps): Lock file maintenance (#335)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 04:24:39 +00:00
renovate[bot] 99aa6c99d8 chore(deps): Update dependency pinia to v2.2.6 (#334)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-04 01:11:37 +00:00
耗子 e0daee4766 feat: 移动部分方法到utils 2024-11-04 03:04:34 +08:00
耗子 bf8c832b32 feat: 去掉重复的方法 2024-11-04 02:33:15 +08:00
耗子 2d35cbc176 fix: lint 2024-11-03 16:46:05 +08:00
renovate[bot] 6b51f58856 chore(deps): Update dependency @types/node to v22.8.7 (#330)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-03 07:25:33 +00:00
renovate[bot] f58c680ced chore(deps): Update dependency @iconify/json to v2.2.267 (#329)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 19:49:40 +00:00
耗子 9c6d0b5837 feat: php拓展安装添加确认弹窗 2024-11-02 21:28:29 +08:00
耗子 d069cbec75 feat: 负载条不允许超过100% 2024-11-02 21:22:35 +08:00
耗子 9b252aa8f6 docs: 更新文档 2024-11-02 20:02:43 +08:00
耗子 a52d06e3cb feat: 添加memcached php拓展 2024-11-02 19:26:38 +08:00
耗子 c25c78bfef feat: 发布v2.3.17 2024-11-02 18:32:54 +08:00
耗子 567f3ab693 fix: lint 2024-11-02 18:18:29 +08:00
耗子 aa162e12db feat: 支持memcached 2024-11-02 17:48:52 +08:00
renovate[bot] 7169b90c1d chore(deps): Update dependency sass to v1.80.6 (#328)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 03:07:42 +00:00
耗子 5e1001f10f docs: update README 2024-11-02 03:36:33 +08:00
耗子 8e26589c80 feat: 提升最低TLS到1.3 2024-11-02 03:34:21 +08:00
耗子 27ec65650d feat: 替换go默认的logger 2024-11-02 01:12:17 +08:00
耗子 e2345a8285 workflow: 合并codecov与test 2024-11-02 00:43:01 +08:00
耗子 4ebe20d7f8 workflow: issue lock修改为手动触发 2024-11-02 00:37:02 +08:00
renovate[bot] 8f5872b284 chore(deps): Update dependency alova to v3.2.0 (#327)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-02 00:34:24 +08:00
耗子 75c6522453 fix: lint 2024-11-01 23:07:51 +08:00
耗子 ff34cc72cc fix: lint 2024-11-01 21:54:06 +08:00
耗子 e2fb1080da Merge remote-tracking branch 'origin/main' 2024-11-01 21:42:27 +08:00
耗子 b96c6ad899 feat: php支持74和80 2024-11-01 21:42:20 +08:00
renovate[bot] c1a4fb0904 chore(deps): Update all dependencies (#326)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-01 09:46:35 +08:00
renovate[bot] 5c50da27c5 chore(deps): Update dependency @vue-js-cron/naive-ui to v2.0.7 (#325)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 18:48:45 +00:00
耗子 336a9bc675 fix: 文件管理bug 2024-10-31 21:49:58 +08:00
耗子 ea3bf37687 fix: 7z list 2024-10-31 18:38:03 +08:00
耗子 79e09f9cb7 chore: debug 2024-10-31 18:32:08 +08:00
耗子 d73c988073 chore: debug 2024-10-31 18:32:01 +08:00
耗子 9641ae7f34 chore: debug 2024-10-31 18:31:56 +08:00
耗子 fd2c89a480 chore: debug 2024-10-31 18:27:01 +08:00
耗子 526f192200 feat(文件): 解压时只设置压缩包内文件的权限 2024-10-31 18:20:54 +08:00
renovate[bot] 6423953aeb chore(deps): Update dependency @types/node to v22.8.5 (#324)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 17:07:19 +08:00
耗子 1091ec8a23 Merge remote-tracking branch 'origin/main' 2024-10-31 12:35:38 +08:00
耗子 2cbc641b2d feat(文件): 优化解压,保存时的权限设置 2024-10-31 12:35:31 +08:00
renovate[bot] f4d23211e3 chore(deps): Update dependency @vueuse/core to v11.2.0 (#323)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 01:16:52 +00:00
耗子 1b6086bb0b docs: 更新bug模板 2024-10-31 04:11:13 +08:00
耗子 f399e9fcec fix: 应用更新按钮错位 2024-10-31 04:11:03 +08:00
renovate[bot] 17cf1ada0e chore(deps): Update all dependencies (#321)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-31 03:43:36 +08:00
耗子 85bd71aefe feat: 发布v2.3.16 2024-10-31 03:34:02 +08:00
耗子 f01afacf34 feat: 优化docker兼容 2024-10-31 03:28:41 +08:00
耗子 52e8830605 feat: docker 管理器 2024-10-31 01:23:46 +08:00
耗子 e90c6f4f7b fix: 计划任务修改后未刷新 2024-10-30 20:38:35 +08:00
renovate[bot] eb94ef89b3 chore(deps): Update dependency sass to v1.80.5 (#318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-30 01:42:39 +00:00
renovate[bot] adc6dccaae chore(deps): Update all dependencies (#317)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 18:30:35 +00:00
renovate[bot] 855b2e87f9 chore(deps): Update dependency @types/node to v22 (#316)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 11:26:03 +08:00
renovate[bot] 3c601b808f chore(deps): Update dependency @types/node to v20.17.2 (#315)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 02:17:04 +00:00
renovate[bot] 8652501e95 chore(deps): Lock file maintenance (#313)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 00:22:35 +08:00
renovate[bot] 43b67f67be chore(deps): Update dependency md-editor-v3 to v4.21.2 (#312)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 00:20:13 +08:00
renovate[bot] 4fff0b84d8 chore(deps): migrate config renovate.json (#314)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-29 00:03:12 +08:00
renovate[bot] fd73982f86 chore(deps): Update dependency cronstrue to v2.51.0 (#311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 23:52:24 +08:00
renovate[bot] 10191ae5d6 chore(deps): Update dependency eslint-plugin-vue to v9.30.0 (#309)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 23:52:14 +08:00
耗子 b162f71d47 chore: update renovate config 2024-10-28 23:56:07 +08:00
耗子 a9f34a5783 feat: 实验性支持alova 2024-10-28 23:44:48 +08:00
耗子 1e4285b516 fix: mysql数据库为空时无法备份 2024-10-28 21:10:49 +08:00
耗子 44c370a119 feat(#305): 使用slog记录所有日志 2024-10-28 20:18:25 +08:00
renovate[bot] a1c0b4a399 chore(deps): Update dependency @iconify/json to v2.2.265 (#308)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 15:18:03 +08:00
renovate[bot] 805f6c0002 chore(deps): Lock file maintenance (#307)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 05:10:06 +00:00
renovate[bot] 5066446053 chore(deps): Lock file maintenance (#306)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-28 03:09:55 +00:00
耗子 0c31641ebd fix: lint 2024-10-28 03:36:35 +08:00
耗子 343d451752 feat: 所有shell操作全部使用shell包 2024-10-28 03:35:20 +08:00
耗子 12ecf0707e feat: 规范压缩解压 2024-10-28 03:21:22 +08:00
耗子 899ce1fa18 feat: 优化压缩解压错误输出 2024-10-28 02:56:53 +08:00
耗子 9ef56f6ede feat: 适配面板备份 2024-10-28 02:43:21 +08:00
耗子 4b3ebc082c feat: 合并gz和tgz 2024-10-28 02:05:36 +08:00
耗子 fb3c8ccc85 fix: test 2024-10-28 02:01:34 +08:00
耗子 f620a8737e fix: test 2024-10-28 01:55:30 +08:00
耗子 d0d5b237ef fix: test 2024-10-28 01:52:37 +08:00
耗子 11f5adc5a4 fix: test 2024-10-28 01:44:31 +08:00
耗子 6927a0256c fix: test 2024-10-28 01:42:21 +08:00
耗子 dbc78343dc fix: test 2024-10-28 01:39:58 +08:00
耗子 d8347310ea fix: test 2024-10-28 01:36:27 +08:00
耗子 8d6b3fe95e fix: test 2024-10-28 01:28:28 +08:00
耗子 036e01795e fix: test 2024-10-28 01:25:22 +08:00
耗子 2d7faaa653 fix: test 2024-10-28 01:22:56 +08:00
耗子 5921a88c75 fix: test 2024-10-28 01:20:41 +08:00
耗子 d6f4b8736b fix: 尝试修复压缩 2024-10-28 01:20:03 +08:00
耗子 227f10751c feat: 避免压缩包多一层目录 2024-10-28 01:02:04 +08:00
耗子 54cc556d17 feat: 重构压缩解压 2024-10-28 00:10:24 +08:00
耗子 0d8a26ccb6 fix: lint 2024-10-27 23:07:06 +08:00
耗子 eb18b1080a refactor: 使用api获取容器信息 2024-10-27 22:52:42 +08:00
renovate[bot] ffe7b95b9c chore(deps): Update module github.com/bddjr/hlfhr to v1.1.3 (#303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-27 19:24:58 +08:00
耗子 ca097cac1e feat: 移除docker包 2024-10-27 19:23:22 +08:00
耗子 f3a578380a feat: 容器网络使用cli实现 2024-10-27 18:51:53 +08:00
耗子 beb271b3b9 feat: 容器镜像使用cli实现 2024-10-27 18:12:21 +08:00
耗子 310316c8e4 feat: 优化容器结构体 2024-10-27 17:45:36 +08:00
耗子 7521b0f788 feat: 使用cli实现容器管理 2024-10-27 17:33:34 +08:00
耗子 2783e2b28e feat: 删除容器一批无用的接口 2024-10-27 16:35:34 +08:00
耗子 cf26063287 feat: 优化去掉go-yaml依赖 2024-10-27 16:23:52 +08:00
耗子 2a61f8939f fix: PHP关闭状态下获取负载报错 2024-10-27 04:37:38 +08:00
耗子 513a298541 feat: 发布v2.3.15 2024-10-27 04:19:48 +08:00
renovate[bot] 1cbc53b311 chore(deps): Update dependency vue-tsc to v2.1.8 (#302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-27 04:18:37 +08:00
耗子 aed35990bc feat: 支持签发自签名证书 2024-10-27 04:17:58 +08:00
耗子 9179543b7f feat: 支持重签面板HTTPS证书 2024-10-27 04:09:33 +08:00
耗子 f40ad8bdf9 feat: 手动签发证书 2024-10-27 03:17:21 +08:00
耗子 56ae7ebfa0 feat: 支持上传本地证书 2024-10-27 02:35:04 +08:00
耗子 cf5f7bd866 feat: 证书支持同时部署多个网站 2024-10-27 01:46:58 +08:00
耗子 bd6496c1cb feat: 网站支持选择证书填入 2024-10-27 01:22:47 +08:00
耗子 3c75c1c49d feat: 文件名支持缩略显示 2024-10-27 01:08:43 +08:00
耗子 2b0f8a57bb fix: lint 2024-10-27 01:00:13 +08:00
耗子 658b6a79eb feat: 文件操作支持覆盖 2024-10-27 01:00:02 +08:00
耗子 42dd00c5ea feat: 标记的文件可以取消 2024-10-27 00:17:32 +08:00
耗子 8fa5cec568 Merge remote-tracking branch 'origin/main' 2024-10-27 00:02:35 +08:00
耗子 2af4b9569d feat: 不允许把目录复制到原目录中 2024-10-27 00:02:29 +08:00
renovate[bot] 408b9ce19b chore(deps): Update dependency @iconify/json to v2.2.264 (#300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-26 23:30:44 +08:00
耗子 b2845fba15 feat: http自动跳转https 2024-10-26 21:26:14 +08:00
耗子 ba819a5a9c fix: 修复监控单位错误 2024-10-26 20:07:33 +08:00
耗子 bf1359e3d6 fix: 禁用resty的日志输出 2024-10-26 16:33:53 +08:00
耗子 2970aaaa47 feat: 一键签发证书支持自动同步域名 2024-10-26 13:13:29 +08:00
耗子 a65bcf582a feat: PHP网站默认开启防跨站 2024-10-26 05:06:10 +08:00
耗子 145387b3df fix: Alt-Svc头删除不正确 2024-10-26 04:59:28 +08:00
耗子 f7c586e05b feat: 发布v2.3.14 2024-10-26 04:46:16 +08:00
耗子 96c78b6be9 fix: lint 2024-10-26 04:44:17 +08:00
耗子 46ae37a4a1 feat: 优化自动签发证书 2024-10-26 04:39:22 +08:00
耗子 d56b41bfef feat: 为quic自动添加alt-svc 2024-10-26 03:32:33 +08:00
耗子 68beffff22 Merge remote-tracking branch 'origin/main' 2024-10-26 02:53:21 +08:00
耗子 a774253ec3 feat: 优化证书列表账号显示 2024-10-26 02:53:15 +08:00
耗子 9dfc1d3058 Revert "feat: 尝试改善日志性能"
This reverts commit 5ac43df821.
2024-10-26 02:33:18 +08:00
耗子 049f708cae feat: 优化实时日志为500行 2024-10-26 02:32:57 +08:00
renovate[bot] b749a8599f chore(deps): Update dependency @types/node to v20.17.1 (#299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-26 02:30:07 +08:00
耗子 95cf05879d feat: 支持一键签发证书 2024-10-26 02:28:52 +08:00
耗子 9ac5ae4968 feat: nginx解析器支持设置alt-svc 2024-10-26 01:37:51 +08:00
耗子 2491f34b3f fix: lint 2024-10-26 01:23:25 +08:00
耗子 302d0f1bec fix: 防火墙关闭状态下ping状态报错 2024-10-26 01:23:12 +08:00
耗子 5ac43df821 feat: 尝试改善日志性能 2024-10-25 22:31:38 +08:00
耗子 8a1e7ef88b Merge remote-tracking branch 'origin/main' 2024-10-25 21:29:09 +08:00
耗子 b0a4df7ca6 fix: 创建证书账号/dns后刷新异步数据 2024-10-25 21:29:03 +08:00
renovate[bot] 70f41b87ea chore(deps): Update dependency pinia-plugin-persistedstate to v4.1.2 (#297)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-25 18:16:20 +08:00
耗子 ea96634318 fix: lint 2024-10-25 17:21:27 +08:00
耗子 cfff459673 feat: 移除color包 2024-10-25 17:06:04 +08:00
耗子 bba50692b3 fix: test 2024-10-25 16:53:08 +08:00
耗子 595cb03c67 workflow: 去掉warp actions,太慢了 2024-10-25 16:49:32 +08:00
耗子 fa5817febd fix: lint 2024-10-25 16:36:12 +08:00
耗子 9a9fdbdca6 workflow: 支持ipv6网络 2024-10-25 16:32:47 +08:00
耗子 2225e59ca6 fix: 修改权限不使用sudo 2024-10-25 02:43:33 +08:00
耗子 5dce30523b feat: 支持同时输出ipv4 v6地址 2024-10-25 02:41:58 +08:00
耗子 965c39ee08 feat: 创建网站不允许使用443端口 2024-10-25 02:06:27 +08:00
耗子 3507517d5b feat: 优化密码显示,可点击查看 2024-10-24 22:08:30 +08:00
renovate[bot] 17cce7ec2a chore(deps): Update dependency @types/node to v20.17.0 (#293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 09:43:58 +00:00
耗子 a2b30b07bb fix: 首页mysql数据库数量不显示 2024-10-24 14:03:32 +08:00
耗子 ffb7bfda51 Merge remote-tracking branch 'origin/main' 2024-10-24 13:41:16 +08:00
耗子 d36dd8fded feat: 终端使用可变字体 2024-10-24 13:41:10 +08:00
renovate[bot] a5f69db353 chore(deps): Update dependency sass to v1.80.4 (#295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 04:10:09 +00:00
renovate[bot] 4606c63c81 chore(deps): Update dependency npm-run-all2 to v7.0.1 (#294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-24 00:34:58 +00:00
耗子 247000718d feat: 优化默认主题配色 2024-10-24 03:31:53 +08:00
耗子 aedde0d433 feat: 发布v2.3.13 2024-10-24 03:08:23 +08:00
耗子 0b61f2f67c fix: 容器管理多一个空元素 2024-10-24 03:04:55 +08:00
耗子 b166022cea feat: 优化监控开关样式 2024-10-24 02:51:53 +08:00
耗子 fb88060824 feat: 大部分视图支持消息总线 2024-10-24 02:24:07 +08:00
耗子 f760dbaf80 refactor: 消息总线 2024-10-24 01:59:52 +08:00
耗子 6050bf611b feat: 重构证书组件 2024-10-24 01:48:57 +08:00
耗子 0b425f09ee feat: 移除思源黑体,减小打包大小 2024-10-24 00:38:01 +08:00
耗子 f18c39ff60 feat: 缩小面板侧边栏宽度 2024-10-24 00:28:45 +08:00
耗子 9db30ac11b fix: 加回旧版的arg检查 2024-10-23 22:51:18 +08:00
耗子 3c8f606d45 feat: 添加一层root用户检查 2024-10-23 22:38:30 +08:00
renovate[bot] 59c08f0850 chore(deps): Update dependency @vue/eslint-config-prettier to v10.1.0 (#292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 21:54:38 +08:00
renovate[bot] 29a7aa4ca5 chore(deps): Update dependency @types/node to v20.16.15 (#289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 21:54:27 +08:00
renovate[bot] 8eb75a921e chore(deps): Update dependency @iconify/json to v2.2.263 (#291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 11:47:40 +00:00
耗子 86fb1c392e fix: 优化实时日志modal重载 2024-10-23 15:47:53 +08:00
耗子 df4e47cc7b fix: supervisor不支持重载 2024-10-23 15:35:20 +08:00
耗子 6101bedee3 fix: 优化网站列表获取 2024-10-23 15:26:23 +08:00
耗子 817e86466c fix: 网站列表HTTPS显示不正确 2024-10-23 15:21:52 +08:00
耗子 710601600a Merge remote-tracking branch 'origin/main' 2024-10-23 15:09:24 +08:00
耗子 63a934510e fix: 统一重定向状态码为308 2024-10-23 15:09:17 +08:00
renovate[bot] f3aac307d0 chore(deps): Update dependency vite to v5.4.10 (#290)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 13:11:00 +08:00
耗子 e32c887bd7 feat: 终端固定背景颜色防止闪烁 2024-10-23 04:30:14 +08:00
耗子 bc37511b15 fix: 实时日志调整为1000行 2024-10-23 04:24:00 +08:00
耗子 72652eb814 fix: remove debug 2024-10-23 04:18:31 +08:00
耗子 6e3db107ec fix: go embed error 2024-10-23 04:06:04 +08:00
耗子 81843470b7 feat: 发布v2.3.11 2024-10-23 03:13:44 +08:00
renovate[bot] a539532ae2 chore(deps): Update unocss monorepo to v0.63.6 (#288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-23 03:12:40 +08:00
耗子 fc1584e7da fix: 罕见情况下防火墙规则错误 2024-10-23 03:05:40 +08:00
耗子 3c72a66c1f feat: ssh多机管理 2024-10-23 02:39:43 +08:00
renovate[bot] 346ec7ab1f chore(deps): Update dependency @types/node to v20.16.14 (#287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-22 07:07:18 +00:00
renovate[bot] 7850b74966 chore(deps): Update dependency npm-run-all2 to v7 (#286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-22 10:17:28 +08:00
耗子 f2d2bad52f feat: 初始化ssh模型 2024-10-22 02:59:53 +08:00
耗子 0786451c79 docs: 添加微信群 2024-10-22 02:52:56 +08:00
renovate[bot] 0291ae7637 chore(deps): Lock file maintenance (#283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 16:01:50 +08:00
renovate[bot] de76376e97 chore(deps): Update dependency md-editor-v3 to v4.21.1 (#282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 16:01:26 +08:00
耗子 de21179d61 fix: lint 2024-10-21 11:56:53 +08:00
renovate[bot] b81f3de01d chore(deps): Lock file maintenance (#281)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 11:55:54 +08:00
renovate[bot] be3bae7d57 chore(deps): Update module github.com/klauspost/compress to v1.17.11 (#278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:39:07 +08:00
renovate[bot] ffb4e21f48 chore(deps): Update dependency eslint-plugin-vue to v9.29.1 (#279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:38:58 +08:00
renovate[bot] 39624d8e1c chore(deps): Update dependency @iconify/json to v2.2.262 (#280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 01:38:48 +08:00
耗子 1378413e54 fix: lint 2024-10-21 01:35:32 +08:00
耗子 730aea037c feat: 应用支持实时日志 2024-10-21 01:33:13 +08:00
耗子 5ae6135429 fix: 实时日志优化 2024-10-21 01:16:09 +08:00
耗子 84703391a3 feat: 支持实时日志流 2024-10-21 01:08:10 +08:00
耗子 354705ec5b fix: cron添加脚本不保存 2024-10-21 01:07:37 +08:00
耗子 f182eab2dc fix: test 2024-10-20 22:29:25 +08:00
耗子 2828795fb0 fix: test 2024-10-20 22:21:31 +08:00
耗子 e6aad21631 fix: lint 2024-10-20 22:15:23 +08:00
耗子 ff239c467a refactor: 面板websocket 2024-10-20 22:14:11 +08:00
耗子 892f91be3e feat: 开发服务器代理支持多目标 2024-10-20 19:55:14 +08:00
耗子 d51d11f460 feat: 文件远程下载 2024-10-20 19:20:39 +08:00
耗子 f9233bd36b feat: 文件搜索 2024-10-20 18:51:05 +08:00
耗子 a2ebc070ae feat: 文件管理优化 2024-10-20 17:28:02 +08:00
耗子 480a6a829d feat: 终端支持字体缩放 2024-10-20 05:14:48 +08:00
耗子 47b92a8b2a refactor: 重写ssh 2024-10-20 04:38:27 +08:00
耗子 1a7f679fca feat: 发布v2.3.10 2024-10-20 01:47:00 +08:00
耗子 8b6b94ea1f feat: 跑分插件优化 2024-10-20 01:46:25 +08:00
耗子 0d9c8b3a2d feat: 跑分插件 2024-10-20 01:34:22 +08:00
耗子 2be97a4543 feat: 优化颜色选择器 2024-10-19 21:05:25 +08:00
renovate[bot] 7b6b86f0f6 chore(deps): Update dependency @types/node to v20.16.13 (#277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 21:04:55 +08:00
耗子 6f82242d92 feat: 颜色选择器 2024-10-19 21:01:46 +08:00
耗子 7f301cec58 feat: 组件支持固定 2024-10-19 20:47:33 +08:00
耗子 c75cfe9eb6 feat: 前端路由固定 2024-10-19 20:18:23 +08:00
耗子 b94664ed87 Merge remote-tracking branch 'origin/main' 2024-10-19 18:01:26 +08:00
耗子 93fc5e08f6 feat: 前端优化 2024-10-19 18:01:18 +08:00
renovate[bot] 9bcfd4cd73 chore(deps): Update dependency sass to v1.80.3 (#276)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-19 15:35:39 +08:00
耗子 2d10110eef fix: test 2024-10-19 15:01:44 +08:00
耗子 78e31fdc40 chore: 添加aff 2024-10-19 14:41:47 +08:00
耗子 d2b1394950 feat(HTTPS): 证书支持华为云 2024-10-19 05:14:00 +08:00
耗子 389b0a005b feat(HTTPS): 优化证书申请 2024-10-19 03:03:38 +08:00
renovate[bot] 0b1554c2c6 chore(deps): Update dependency npm-run-all2 to v6.2.4 (#274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 17:17:39 +08:00
renovate[bot] 3b580e2827 chore(deps): Update dependency sass to v1.80.2 (#273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-18 08:48:22 +08:00
耗子 595f5ba2d9 fix: tests 2024-10-18 02:28:08 +08:00
耗子 78543dac67 feat: 发布v2.3.9 2024-10-18 02:11:58 +08:00
耗子 e9cbc8e945 refactor: 重构防火墙 2024-10-18 02:08:55 +08:00
耗子 905e206f9c fix: test 2024-10-17 23:27:11 +08:00
耗子 b5b1992e3b fix: build 2024-10-17 23:05:38 +08:00
耗子 a7fe0c87a5 feat: 首页资源总览优化 2024-10-17 22:46:49 +08:00
耗子 5dcbac1c93 feat(工具箱): 支持设置主机名和时间 2024-10-17 22:17:20 +08:00
耗子 67a6d5cea8 feat: 添加应用图标 2024-10-17 21:22:26 +08:00
耗子 f6fec06e82 feat: 添加应用图标 2024-10-17 21:20:26 +08:00
耗子 2d74e8966e fix: 优化表格样式 2024-10-17 20:30:56 +08:00
耗子 065877547e fix: 固定按钮防止错位 2024-10-17 20:17:06 +08:00
耗子 12b6a1af35 feat: 优化首页布局 2024-10-17 18:55:08 +08:00
耗子 b36f7f3085 feat: 添加密码复杂度验证 2024-10-17 18:40:27 +08:00
耗子 2112e86329 fix: 修复报错Slug不存在 2024-10-17 18:02:48 +08:00
耗子 f2c3569447 fix: 修改部分端口报错 2024-10-17 17:54:33 +08:00
耗子 1a9fe40b80 feat: cli支持更新应用 2024-10-17 17:32:38 +08:00
耗子 df021819b4 feat: 大幅优化移动端体验 2024-10-17 17:18:54 +08:00
耗子 57749f03b9 fix: 防火墙创建规则后未关闭窗口 2024-10-17 16:30:06 +08:00
耗子 d97697efde feat: 发布v2.3.8 2024-10-17 16:25:14 +08:00
耗子 bc28719b10 fix: 修改端口报错 2024-10-17 16:22:01 +08:00
耗子 db79d17919 feat: 支持openEuler 2024-10-17 16:19:22 +08:00
耗子 4e6654af33 fix: 华为欧拉检查 2024-10-17 13:51:41 +08:00
耗子 4015c7e1bc Merge remote-tracking branch 'origin/main' 2024-10-17 13:20:30 +08:00
耗子 d30874a5d4 fix: opencloudos检查 2024-10-17 13:20:24 +08:00
renovate[bot] f4a97c1740 chore(deps): Update dependency sass to v1.80.1 (#272)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 12:49:54 +08:00
耗子 7cb497929b fix: cli下安装应用 2024-10-17 12:39:12 +08:00
耗子 8cb74159f2 feat: 发布v2.3.7 2024-10-17 12:28:37 +08:00
renovate[bot] 3b75f7d2ac chore(deps): Update dependency sass to v1.80.0 (#271)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 12:15:24 +08:00
耗子 87c65176aa Merge remote-tracking branch 'origin/main' 2024-10-17 12:09:06 +08:00
耗子 3b3cb1ff24 fix: CLI下不运行队列分发 2024-10-17 12:09:00 +08:00
renovate[bot] a701ebcad6 chore(deps): Update dependency @types/node to v20.16.12 (#270)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 03:18:31 +00:00
renovate[bot] 3d88983feb chore(deps): Update dependency @iconify/json to v2.2.261 (#268)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-17 01:37:10 +00:00
耗子 39a9bad2e1 fix: 去掉翻译 2024-10-17 04:45:30 +08:00
耗子 7b0e98bf02 feat: 防火墙完善 2024-10-17 04:44:58 +08:00
耗子 324c61625d feat: 防火墙后端升级 2024-10-17 02:05:27 +08:00
耗子 b50a3f4b4e fix: 首页监控默认10条 2024-10-17 01:13:28 +08:00
耗子 3de3b40d88 feat: 首页全新设计 2024-10-17 00:50:43 +08:00
耗子 722c4f3812 feat: 首页全新设计 2024-10-16 22:54:57 +08:00
耗子 9cf3a0e2b6 fix: 防火墙删不掉 2024-10-16 21:51:37 +08:00
耗子 c80c22c133 refactor: 仪表盘实时数据接口 2024-10-16 17:04:34 +08:00
耗子 5acf1e6eb7 feat: 修改面板端口自动放行 2024-10-16 15:22:47 +08:00
耗子 cdeaf9f48f feat: 完善验证器 2024-10-16 04:09:59 +08:00
耗子 afebeb7c00 feat: 发布v2.3.6 2024-10-16 02:33:43 +08:00
耗子 0d73f2919d workflow: update auto-close 2024-10-16 02:22:12 +08:00
耗子 070ad97e95 workflow: 添加回修订的auto-close 2024-10-16 01:52:52 +08:00
耗子 b7eba92b2b Revert "workflow: 暂时移除auto-close威力太大了"
This reverts commit 2c339b9923.
2024-10-16 01:47:42 +08:00
耗子 2c339b9923 workflow: 暂时移除auto-close威力太大了 2024-10-16 01:28:16 +08:00
耗子 8d0ea50d4f workflow: 添加自动关闭issue 2024-10-16 01:19:44 +08:00
耗子 d6a1c13328 workflow: 添加自动关闭issue 2024-10-16 01:02:37 +08:00
耗子 789e6e2043 docs: 优化描述 2024-10-15 23:55:38 +08:00
耗子 d692b9ac13 docs: 添加自动挂载脚本说明 2024-10-15 23:55:15 +08:00
耗子 eb362e42e3 fix: #260 2024-10-15 18:07:31 +08:00
耗子 3ca106ec87 refactor: 重构证书目录为cert 2024-10-15 18:05:14 +08:00
耗子 377baa0783 fix: build 2024-10-15 17:52:34 +08:00
耗子 117ccabbb5 feat: 优化默认分页条数 2024-10-15 17:51:25 +08:00
耗子 b718c11f3a feat: 离线模式 2024-10-15 17:44:41 +08:00
耗子 41d3d3fb97 feat: 重启后自动调度等待中的任务 2024-10-15 17:18:28 +08:00
耗子 31ad944da8 Merge remote-tracking branch 'origin/main' 2024-10-15 16:53:52 +08:00
耗子 a25de6e4db feat: 仅在cli中打印信息 2024-10-15 16:53:42 +08:00
renovate[bot] 3af6d4d47d chore(deps): Update dependency marked to v14.1.3 (#259)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 16:01:11 +08:00
耗子 59a2fef9e8 chore: 优化描述 2024-10-15 11:35:41 +08:00
耗子 3d22a1bc03 chore: 优化描述 2024-10-15 10:11:14 +08:00
耗子 e64c27a821 chore: 添加新赞助商 2024-10-15 10:09:22 +08:00
耗子 f2bdba10bb chore: 添加新赞助商 2024-10-15 10:08:12 +08:00
耗子 e18d07e694 chore: 添加新赞助商 2024-10-15 10:06:05 +08:00
耗子 50ba0ce38d feat: 优化修复流程 2024-10-15 03:46:56 +08:00
耗子 8b2c7ea778 feat: 优化备份信息终端输出 2024-10-15 03:38:52 +08:00
耗子 e640a3cedd feat: 添加更新日期 2024-10-15 03:26:31 +08:00
耗子 31ab4f0fb6 feat: 发布v2.3.5 2024-10-15 03:24:27 +08:00
耗子 8dee17b539 Merge remote-tracking branch 'origin/main' 2024-10-15 03:18:15 +08:00
renovate[bot] af3172739e chore(deps): Update dependency vite to v5.4.9 (#258)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-15 03:16:41 +08:00
耗子 182d9b1c8c feat: 添加面板修复命令 2024-10-15 03:14:38 +08:00
耗子 d03facb556 feat: 优化升级流程 2024-10-15 02:23:24 +08:00
耗子 577fb370a7 feat: 发布v2.3.4 2024-10-15 00:44:24 +08:00
耗子 7ef8fc6787 fix: lint 2024-10-15 00:36:41 +08:00
耗子 e5ba1375b1 feat: 网站管理优化 2024-10-15 00:29:12 +08:00
耗子 e8a01e2d04 refactor: 网站nginx配置解析生成 2024-10-14 21:34:24 +08:00
耗子 bdee27541c feat: nginx解析器完成 2024-10-14 19:07:48 +08:00
renovate[bot] f807da175d chore(deps): Lock file maintenance (#256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 15:13:19 +08:00
renovate[bot] ae29541d02 chore(deps): Update dependency @iconify/json to v2.2.260 (#257)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 15:12:49 +08:00
renovate[bot] 0f844ac3ea chore(deps): Update module github.com/urfave/cli/v3 to v3.0.0-alpha9.1 (#255)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-14 15:12:35 +08:00
耗子 744d0b9617 fix: 跨文件系统文件移动优化 2024-10-14 13:03:16 +08:00
耗子 1bea44146f feat: 初步实现nginx配置解析器 2024-10-14 03:32:51 +08:00
耗子 a7a68f6b34 feat: 添加请求验证标签 2024-10-13 23:43:59 +08:00
407 changed files with 18180 additions and 9217 deletions
+12 -13
View File
@@ -19,15 +19,13 @@ body:
options:
- label: 我已经搜索了现有的 Issues, Discussions 和 Google (I've searched the existing Issues, Discussions and Google)
required: true
- label: 我已经阅读了 README 中除版本记录以外的所有内容 (I've read everything in the README except the version notes)
required: true
- label: 我在提问题之前至少花费了 5 分钟来思考和准备 (I spend at least 5 minutes thinking and preparing before asking a question)
- label: 我已经阅读了文档中除版本记录以外的所有内容 (I've read everything in the documentation except the version notes)
required: true
- label: 我没有改动过面板及系统的任何文件 (I have not changed any files of the panel and system)
required: false
- label: 这个问题可以被稳定复现 (The problem can be stably reproduced)
required: false
- label: 问题是在升级之后产生的 (The problem is generated after upgrading)
- label: 问题是在更新之后产生的 (The problem is generated after upgrading)
required: false
- type: dropdown
id: system
@@ -37,9 +35,10 @@ body:
请提供安装面板的操作系统。
Please provide the operating system on which the panel is installed.
options:
- RockyLinux
- AlmaLinux
- Debian
- Ubuntu
- AlmaLinux
- RockyLinux
- Other
validations:
required: true
@@ -50,17 +49,17 @@ body:
description: |
请提供操作系统的版本号。
Please provide the version number of the operating system.
placeholder: "9.0"
placeholder: "12"
validations:
required: true
- type: input
id: version
attributes:
label: 耗子面板版本 (HaoZi Panel Version)
label: 耗子面板版本 (Rat Panel Version)
description: |
请提供面板的版本号。
Please provide the version number of the panel.
placeholder: "2.0.0"
placeholder: "2.3.0"
validations:
required: true
- type: textarea
@@ -68,7 +67,7 @@ body:
attributes:
label: 描述问题 (Describe The Problem)
description: |
简明概要地描述遇到的问题。
简明概要地描述遇到的问题。
Briefly describe the problem you are having.
validations:
required: true
@@ -91,7 +90,7 @@ body:
attributes:
label: 预期行为 (Expected Behavior)
description: |
简明概要地描述期望发生的事情。
简明概要地描述期望发生的事情。
Briefly describe what you expect to happen.
validations:
required: true
@@ -113,7 +112,7 @@ body:
attributes:
label: 截图 (Screenshots)
description: |
如果有,添加屏幕截图可帮助更快定位的问题。
如果有,添加屏幕截图可帮助更快定位的问题。
If so, adding screenshots can help locate your issue faster.
可以复制图片后在此区域内粘贴以添加图片。
Pictures can be copied and pasted in this area to add pictures.
@@ -128,7 +127,7 @@ body:
description: |
运行环境?浏览器?软件版本?相关的配置?链接?参考资料?
Environment? Browser? Software version? Related configuration? Link? References?
任何能让我们对所遇到的问题有更多了解的东西。
任何能让我们对所遇到的问题有更多了解的东西。
Anything that can give us more insight into the problem you're having.
validations:
required: false
+1 -1
View File
@@ -25,7 +25,7 @@ body:
attributes:
label: 描述功能 (Describe Feature)
description: |
简明概要地描述的新功能,以及它将解决什么问题。
简明概要地描述的新功能,以及它将解决什么问题。
Briefly describe your new feature and what problem it will solve.
validations:
required: true
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

-26
View File
@@ -1,26 +0,0 @@
name: Codecov
on:
push:
branches:
- main
pull_request:
jobs:
codecov:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
cache: true
go-version: 'stable'
- name: Install dependencies
run: go mod tidy
- name: Run tests with coverage
run: go test -v -coverprofile="coverage.out" ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.out
token: ${{ secrets.CODECOV }}
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
# We need to run the dev server first to generate the auto-imports files
run: |
cp .env.production .env
cp settings/proxy-config.ts.example settings/proxy-config.ts
cp settings/proxy-config.example.ts settings/proxy-config.ts
pnpm dev &
sleep 5
kill %1
+22
View File
@@ -0,0 +1,22 @@
name: Issue Auto Lock
on:
schedule:
- cron: "0 */6 * * *"
workflow_dispatch:
issues:
types: [ opened ]
permissions:
issues: write
contents: read
jobs:
issue-lock:
runs-on: ubuntu-latest
steps:
- name: Set GH_REPO
run: echo "GH_REPO=${{ github.repository }}" >> $GITHUB_ENV
- name: Auto Lock Issue
uses: devhaozi/issue-auto-lock@v1
with:
gh_repo: ${{ github.repository }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
issue_labels: "⭐ No Star"
+6 -1
View File
@@ -21,4 +21,9 @@ jobs:
run: |
cp config.example.yml config.yml
- name: Run tests
run: go test ./...
run: sudo go test -v -coverprofile="coverage.out" ./...
- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.out
token: ${{ secrets.CODECOV }}
+65 -30
View File
@@ -16,9 +16,9 @@
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2FTheTNB%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2FTheTNB%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
</p>
耗子面板是使用 Golang + Vue 开发的开源轻量 Linux 服务器运维管理面板
耗子面板是新一代全能服务器运维管理面板。简单轻量,高效运维
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.com/a/TheTNB)
QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 微信群:[复制此链接](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.com/a/TheTNB)
## 优势
@@ -26,13 +26,13 @@
2. **低破坏性:** 面板的设计理念是尽可能减少对系统的额外修改,在同类面板中,我们对系统的修改最少。
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障。
4. **高效运维:** 面板界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
5. **离线运行:** 面板运行可不依赖任何外部服务,甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
5. **离线运行:** 面板运行可不依赖任何外部服务,甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 2 年无事故。
7. **开源开放:** 面板开源,可以自由修改、审计面板源码,安全性有保障。
7. **开源开放:** 面板开源,可以自由修改、审计面板源码,安全性有保障。
## UI 截图
![UI 截图](ui.png)
![UI 截图](.github/assets/ui.png)
## 运行环境
@@ -46,30 +46,37 @@
|---------------------|-----|-----|
| AlmaLinux | 9 | 推荐 |
| AlmaLinux | 8 | 不推荐 |
| RockyLinux | 9 | |
| RockyLinux | 9 | 支持 |
| RockyLinux | 8 | 不推荐 |
| CentOS Stream | 9 | 不推荐 |
| CentOS Stream | 8 | 不推荐 |
| Ubuntu | 24 | 推荐 |
| Ubuntu | 22 | |
| Ubuntu | 22 | 支持 |
| Debian | 12 | 推荐 |
| Debian | 11 | |
| OpenCloudOS | 9 | |
| TencentOS Server | 4 | |
| Debian | 11 | 支持 |
| OpenCloudOS | 9 | 支持 |
| TencentOS Server | 4 | 支持 |
| TencentOS Server | 3.1 | 不推荐 |
| Alibaba Cloud Linux | 3.2 | 不推荐 |
| Anolis | 8 | 不推荐 |
| openEuler | 22 | 不推荐 |
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的健壮性。
## 挂载硬盘
如果您的服务器有未挂载的数据盘,可在安装前以`root`用户登录服务器运行以下命令自动挂载,面板安装后不支持跨目录迁移。
```shell
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
```
## 安装面板
> **Warning**
> 安装面板前,需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
> 安装面板前,需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
如果你决定继续,请`root`用户登录服务器,行以下命令安装面板:
**当前 v2.3.x 为测试版本,欢迎帮助我们测试在不同操作系统下的兼容性。**
`root`用户登录服务器,行以下命令安装面板:
```shell
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
@@ -79,7 +86,7 @@ curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash in
优先建议备份数据重装系统,这样可以保证系统纯净。
如果无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
如果无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
```shell
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
@@ -105,19 +112,41 @@ panel-cli
## 赞助商
如果耗子面板对有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
如果耗子面板对有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
**同时接受云资源赞助,可通过QQ群咨询联系**
### 资金
- [微晓朵](https://www.weixiaoduo.com/)
<a href="https://www.weixiaoduo.com/">
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
</a>
### 服务器
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
<img height="80" src=".github/assets/dk.png" alt="林枫云">
</a>
### CDN
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
- [WAF PRO](https://waf.pro/)
- [盾云SCDN](https://scdn.ddunyun.com/)
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
</a>
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
</a>
<a href="https://scdn.ddunyun.com/">
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
</a>
### 合作伙伴
<a href="https://1ms.run">
<img height="80" src=".github/assets/1ms.svg" alt="毫秒镜像提供经过审核的 Docker 镜像加速服务">
</a>
### 赞助商们
<p align="center">
<a target="_blank" href="https://afdian.com/a/TheTNB">
@@ -129,19 +158,25 @@ panel-cli
这个项目的存在要归功于所有做出贡献的人,参与贡献请先查看贡献代码部分。
<a href="https://github.com/TheTNB/panel/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=TheTNB/panel"/>
</a>
<p align="center">
<a href="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors?repo_id=572922963&limit=30" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=dark" width="655" height="auto">
<img alt="Active Contributors of TheTNB/panel - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=light" width="655" height="auto">
</picture>
</a>
</p>
## Star 历史
<a href="https://star-history.com/#TheTNB/panel&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
</picture>
</a>
<p align="center">
<a href="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history?repo_id=572922963" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=dark" width="721" height="auto">
<img alt="Star History of TheTNB/panel" src="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=light" width="721" height="auto">
</picture>
</a>
</p>
## 免责声明
+60 -25
View File
@@ -16,9 +16,9 @@
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2FTheTNB%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2FTheTNB%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
</p>
The Rat Panel is an open source lightweight Linux server operation and maintenance management panel developed using Golang and Vue.
Rat Panel is a new generation of all-in-one server operation and maintenance management panel. Simple and lightweight, efficient operation and maintenance.
Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | Forum: [tom.moe](https://tom.moe) | Sponsor: [Open Collective](https://opencollective.com/tnb)
QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | WeChat group: [Copy this link](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | Forum: [tom.moe](https://tom.moe) | Sponsor: [Open Collective](https://opencollective.com/tnb)
## Advantages
@@ -32,7 +32,7 @@ Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | For
## UI Screenshots
![UI Screenshots](ui.png)
![UI Screenshots](.github/assets/ui.png)
## Operating Environment
@@ -46,30 +46,37 @@ For other systems not listed in the table below, you can try to install them by
|---------------------|---------|-----------------|
| AlmaLinux | 9 | Recommended |
| AlmaLinux | 8 | Not recommended |
| RockyLinux | 9 | |
| RockyLinux | 9 | Support |
| RockyLinux | 8 | Not recommended |
| CentOS Stream | 9 | Not recommended |
| CentOS Stream | 8 | Not recommended |
| Ubuntu | 24 | Recommended |
| Ubuntu | 22 | |
| Ubuntu | 22 | Support |
| Debian | 12 | Recommended |
| Debian | 11 | |
| OpenCloudOS | 9 | |
| TencentOS Server | 4 | |
| Debian | 11 | Support |
| OpenCloudOS | 9 | Support |
| TencentOS Server | 4 | Support |
| TencentOS Server | 3.1 | Not recommended |
| Alibaba Cloud Linux | 3.2 | Not recommended |
| Anolis | 8 | Not recommended |
| openEuler | 22 | Not recommended |
As system versions are constantly updated, we may also terminate support for some older systems to ensure the robustness of the panel.
## Mount Disk
If your server has an unmounted data disk, you can run the following command as the `root` user to automatically mount it before installation. The panel does not support cross-directory migration after installation.
```shell
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
```
## Install Panel
> **Warning**
> Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. We are not recommended for users with zero basic knowledge to install and use Rat Panel.
If you decide to continue, please log in to the server as `root` user and execute the following command to install the panel:
**Current v2.3.x is a test version, welcome to help us test compatibility on different operating systems.**
Login to the server as the `root` user and run the following command to install the panel:
```shell
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
@@ -111,13 +118,35 @@ If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollecti
### Financial
- [微晓朵](https://www.weixiaoduo.com/)
<a href="https://www.weixiaoduo.com/">
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
</a>
### Server
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
<img height="80" src=".github/assets/dk.png" alt="林枫云">
</a>
### CDN
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
- [WAF PRO](https://waf.pro/)
- [盾云SCDN](https://scdn.ddunyun.com/)
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
</a>
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
</a>
<a href="https://scdn.ddunyun.com/">
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
</a>
### Partners
<a href="https://1ms.run">
<img height="80" src=".github/assets/1ms.svg" alt="毫秒镜像提供经过审核的 Docker 镜像加速服务">
</a>
### Sponsors
<p align="center">
<a target="_blank" href="https://afdian.com/a/TheTNB">
@@ -129,19 +158,25 @@ If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollecti
This project owes its existence to all those who have contributed. To contribute, please check the contributed code section first.
<a href="https://github.com/TheTNB/panel/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=TheTNB/panel"/>
</a>
<p align="center">
<a href="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors?repo_id=572922963&limit=30" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=dark" width="655" height="auto">
<img alt="Active Contributors of TheTNB/panel - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=light" width="655" height="auto">
</picture>
</a>
</p>
## Star History
<a href="https://star-history.com/#TheTNB/panel&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
</picture>
</a>
<p align="center">
<a href="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history?repo_id=572922963" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=dark" width="721" height="auto">
<img alt="Star History of TheTNB/panel" src="https://next.ossinsight.io/widgets/official/analyze-repo-stars-history/thumbnail.png?repo_id=572922963&image_size=auto&color_scheme=light" width="721" height="auto">
</picture>
</a>
</p>
## Disclaimer
-2
View File
@@ -1,5 +1,3 @@
# 警告:未经指导私自修改此配置文件可能导致严重后果
# Warning: Modifying this configuration file without guidance may cause serious consequences
app:
debug: false
key: a-long-string-with-32-characters
+25 -65
View File
@@ -3,9 +3,9 @@ module github.com/TheTNB/panel
go 1.23
require (
github.com/bddjr/hlfhr v1.1.3
github.com/beevik/ntp v1.4.3
github.com/docker/docker v27.3.1+incompatible
github.com/docker/go-connections v0.5.0
github.com/creack/pty v1.1.24
github.com/expr-lang/expr v1.16.9
github.com/glebarez/sqlite v1.11.0
github.com/go-chi/chi/v5 v5.1.0
@@ -16,120 +16,80 @@ require (
github.com/go-rat/chix v1.1.4
github.com/go-rat/gormstore v1.0.6
github.com/go-rat/sessions v1.0.11
github.com/go-rat/utils v1.0.3
github.com/go-rat/utils v1.0.7
github.com/go-resty/resty/v2 v2.15.3
github.com/go-sql-driver/mysql v1.8.1
github.com/goccy/go-yaml v1.12.0
github.com/gookit/color v1.5.4
github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a
github.com/gomodule/redigo v1.9.2
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.1
github.com/knadh/koanf/v2 v2.1.2
github.com/lib/pq v1.10.9
github.com/libdns/alidns v1.0.3
github.com/libdns/cloudflare v0.1.1
github.com/libdns/dnspod v0.0.3
github.com/libdns/huaweicloud v0.2.2
github.com/libdns/libdns v0.2.2
github.com/libdns/tencentcloud v1.0.0
github.com/libdns/tencentcloud v1.1.0
github.com/mholt/acmez/v2 v2.0.3
github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/orandin/slog-gorm v1.4.0
github.com/robfig/cron/v3 v3.0.1
github.com/samber/lo v1.47.0
github.com/sethvargo/go-limiter v1.0.0
github.com/shirou/gopsutil v2.21.11+incompatible
github.com/spf13/cast v1.7.0
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v3 v3.0.0-alpha9
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8
github.com/urfave/cli/v3 v3.0.0-alpha9.3
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.28.0
golang.org/x/net v0.30.0
go.uber.org/zap/exp v0.3.0
golang.org/x/crypto v0.29.0
golang.org/x/net v0.31.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.12
moul.io/zapgorm2 v1.3.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/bodgit/plumbing v1.2.0 // indirect
github.com/bodgit/sevenzip v1.3.0 // indirect
github.com/bodgit/windows v1.0.0 // indirect
github.com/connesc/cipherio v0.2.1 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.10.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-rat/securecookie v1.0.1 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/gofiber/schema v1.2.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jaevor/go-nanoid v1.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/knadh/koanf/maps v0.1.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/nrdcg/dnspod-go v0.4.0 // indirect
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // 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/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1033 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1033 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
go.opentelemetry.io/otel/metric v1.30.0 // indirect
go.opentelemetry.io/otel/sdk v1.30.0 // indirect
go.opentelemetry.io/otel/trace v1.30.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.1 // indirect
modernc.org/libc v1.60.1 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.32.0 // indirect
)
replace github.com/mholt/acmez/v2 => github.com/TheTNB/acmez/v2 v2.0.0-20241012163130-5833d84639e0
replace github.com/mholt/acmez/v2 => github.com/TheTNB/acmez/v2 v2.0.0-20241025203320-cc718c4c870b
+92 -402
View File
@@ -1,97 +1,36 @@
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 v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
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/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
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/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/TheTNB/acmez/v2 v2.0.0-20241012163130-5833d84639e0 h1:o66ZhpjXVs7oi3VyiZvlpp9StFFe4KTeklzB/T4s59Q=
github.com/TheTNB/acmez/v2 v2.0.0-20241012163130-5833d84639e0/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/TheTNB/acmez/v2 v2.0.0-20241025203320-cc718c4c870b h1:1iVxCglpEJ/kq48YcujkWq8SeOacMuMmuekMmGyOSPA=
github.com/TheTNB/acmez/v2 v2.0.0-20241025203320-cc718c4c870b/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
github.com/bddjr/hlfhr v1.1.3 h1:O1Vxi7Qf0tOs9oGoSb3Q9KAGTSfh/EXyVMVSO9V1m90=
github.com/bddjr/hlfhr v1.1.3/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/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM=
github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8=
github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY=
github.com/bodgit/sevenzip v1.3.0/go.mod h1:omwNcgZTEooWM8gA/IJ2Nk/+ZQ94+GsytRzOJJ8FBlM=
github.com/bodgit/windows v1.0.0 h1:rLQ/XjsleZvx4fR1tB/UxQrK+SJ2OFHzfPjLWWOhDIA=
github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw=
github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
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/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7 h1:AjCMD0AEGHHbcIpvRcrI8rMA+Us5F4uUt3B3KSpiYN8=
github.com/devhaozi/huaweicloud-sdk-go-v3 v0.0.0-20241018211007-bbebb6de5db7/go.mod h1:Gz/Ng6WCvYc1Q2smuPvow9kszfyH47wtnpZzvdD2juA=
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/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
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/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
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.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
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/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -110,100 +49,46 @@ github.com/go-rat/securecookie v1.0.1 h1:HW0fpKmB+FjJzXTw8ABOwBJ+XrPmRBSZqHhmrv8
github.com/go-rat/securecookie v1.0.1/go.mod h1:tP/ObWYyjmcpabQ7WTon/i2lBSip/Aolliw2llXuPDU=
github.com/go-rat/sessions v1.0.11 h1:2eTUvhEBtymrAtwv71LlGHNylz+Ad2SgY3jg/sSUboA=
github.com/go-rat/sessions v1.0.11/go.mod h1:Ray/GCbuhm4U9xpjFFSCfOTCEn91puEhAXX5creHE9g=
github.com/go-rat/utils v1.0.3 h1:SqH/O0KYq4SBv8naSAjJA4hC45/d8NLNrTCONfqjbFM=
github.com/go-rat/utils v1.0.3/go.mod h1:4WNPlrF57KmeGZN3HOeBgdBVLJL3xgba4QRP/t6pKrE=
github.com/go-rat/utils v1.0.7 h1:hZUe+n1W/tvF3j+oP/TeAHX3AF98SjVB1FuF11zkCAI=
github.com/go-rat/utils v1.0.7/go.mod h1:b0S/vd2bXWvPUQpw8z6RmJrUG7G7BkMuNSrsz6JRBHs=
github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8=
github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
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-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/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/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
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/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
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.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/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/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/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/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/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
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/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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
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.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
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.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/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=
@@ -218,338 +103,148 @@ 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/dnspod v0.0.3 h1:xJHDIujgLjvZnpB8/rMoCHUqA/KxSGBqRUXxSIzNzAA=
github.com/libdns/dnspod v0.0.3/go.mod h1:XLnqMmK7QlLPEbHwcOxbRvlzRvDgaaUlthRNFOPjXPI=
github.com/libdns/huaweicloud v0.2.2 h1:DvaWLiPJ0hoOvvPafKXgWv4gG33O8cqEu87GJeXYtbc=
github.com/libdns/huaweicloud v0.2.2/go.mod h1:F9vNIca+Cq1ZJiMCSkHQUdvXEGY6UbLoaYI3Py9CFDs=
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/libdns/libdns v0.2.1/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/libdns/tencentcloud v1.0.0 h1:u4LXnYu/lu/9P5W+MCVPeSDnwI+6w+DxYhQ1wSnQOuU=
github.com/libdns/tencentcloud v1.0.0/go.mod h1:NlCgPumzUsZWSOo1+Q/Hfh8G6TNRAaTUeWQdg6LbtUI=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/libdns/tencentcloud v1.1.0 h1:UvrU9HR3Cc87vIkj/FrTGkaZigGcRyZFdLCYqKDac1c=
github.com/libdns/tencentcloud v1.1.0/go.mod h1:PuP62hTGHSI3OqSUsrzuvsCWMD+9BlPtVjKasOrAtJ4=
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/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM=
github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A=
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/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
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/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/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U=
github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
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/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU=
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/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/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
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/shirou/gopsutil v2.21.11+incompatible h1:lOGOyCG67a5dv2hq5Z1BLDUqqKp3HkbjPcz5j6XMS0U=
github.com/shirou/gopsutil v2.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
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/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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 h1:C0GHdLTfikLVoEzfhgPfrZ7LwlG0xiCmk6iwNKE+xs0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1033 h1:g263/dapUpOAZJa1Y9x07WgfOl7Yy+FM5Mrf4WyttS8=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1033/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1033 h1:bRCo+X6i/6B+5Q/G4+rexcMRxpSq+9XS/8+Usn6OcOA=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1033/go.mod h1:ta9FOsQP/SvQAhDD6IhVhYWyY+eboPEbPWq7zpiNwZA=
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.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli/v3 v3.0.0-alpha9 h1:P0RMy5fQm1AslQS+XCmy9UknDXctOmG/q/FZkUFnJSo=
github.com/urfave/cli/v3 v3.0.0-alpha9/go.mod h1:0kK/RUFHyh+yIKSfWxwheGndfnrvYSmYFVeKCh03ZUc=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8 h1:7yw1yHkylDcu/CwY4hEEe8MycDLo7B9LjcqwhoL8NrM=
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8/go.mod h1:itu4KWRgrfEwGcfNka+rV4houuirUau53i0diN4lG5g=
github.com/urfave/cli/v3 v3.0.0-alpha9.3 h1:RfQlgUHMRxDMwEEmGsrHd+mXYJpWpXlcJM8w86cpjGs=
github.com/urfave/cli/v3 v3.0.0-alpha9.3/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
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.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8=
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
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-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
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/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
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.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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-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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
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.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
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/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/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/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.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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/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.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
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.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
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/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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
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-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-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-20190524140312-2c0ae7006135/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-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/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-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
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.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
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/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
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/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
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/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
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=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
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/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/yaml.v2 v2.2.2/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-20210107192922-496545a6307b/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.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
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.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
@@ -574,8 +269,3 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
moul.io/zapgorm2 v1.3.0 h1:+CzUTMIcnafd0d/BvBce8T4uPn6DQnpIrz64cyixlkk=
moul.io/zapgorm2 v1.3.0/go.mod h1:nPVy6U9goFKHR4s+zfSo1xVFaoU7Qgd5DoCdOfzoCqs=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+15 -2
View File
@@ -1,13 +1,14 @@
package app
import (
"log/slog"
"github.com/go-chi/chi/v5"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
"github.com/go-rat/sessions"
"github.com/knadh/koanf/v2"
"github.com/robfig/cron/v3"
"go.uber.org/zap"
"gorm.io/gorm"
"github.com/TheTNB/panel/pkg/queue"
@@ -22,12 +23,24 @@ var (
Session *sessions.Manager
Cron *cron.Cron
Queue *queue.Queue
Logger *zap.Logger
Logger *slog.Logger
)
// 定义面板状态常量
const (
StatusNormal = iota
StatusMaintain
StatusClosed
StatusUpgrade
StatusFailed
)
// 面板全局变量
var (
Key string
Root string
Version string
Locale string
IsCli bool
Status = StatusNormal
)
+18
View File
@@ -0,0 +1,18 @@
package benchmark
import (
"github.com/go-chi/chi/v5"
"github.com/TheTNB/panel/pkg/apploader"
"github.com/TheTNB/panel/pkg/types"
)
func init() {
apploader.Register(&types.App{
Slug: "benchmark",
Route: func(r chi.Router) {
service := NewService()
r.Post("/test", service.Test)
},
})
}
+6
View File
@@ -0,0 +1,6 @@
package benchmark
type Test struct {
Name string `json:"name" validate:"required,oneof=image machine compile encryption compression physics json memory disk"`
Multi bool `json:"multi"`
}
+685
View File
@@ -0,0 +1,685 @@
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/TheTNB/panel/internal/service"
)
type Service struct {
}
func NewService() *Service {
return &Service{}
}
// Test 运行测试
func (s *Service) 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 *Service) calculateCpuScore(duration time.Duration) int {
score := int((10 / duration.Seconds()) * float64(3000))
if score < 0 {
score = 0
}
return score
}
// calculateScore 计算内存/硬盘成绩
func (s *Service) calculateScore(duration time.Duration) int {
score := int((20 / duration.Seconds()) * float64(30000))
if score < 0 {
score = 0
}
return score
}
// 图像处理
func (s *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) encryptionTestTask(numThreads int) error {
key := []byte("abcdefghijklmnopqrstuvwxyz123456")
dataSize := 1 * 1024 * 1024 * 1024 // 1GB
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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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 *Service) 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
}
+19
View File
@@ -0,0 +1,19 @@
package docker
import (
"github.com/go-chi/chi/v5"
"github.com/TheTNB/panel/pkg/apploader"
"github.com/TheTNB/panel/pkg/types"
)
func init() {
apploader.Register(&types.App{
Slug: "docker",
Route: func(r chi.Router) {
service := NewService()
r.Get("/config", service.GetConfig)
r.Post("/config", service.UpdateConfig)
},
})
}
+5
View File
@@ -0,0 +1,5 @@
package docker
type UpdateConfig struct {
Config string `form:"config" json:"config" validate:"required"`
}
+45
View File
@@ -0,0 +1,45 @@
package docker
import (
"net/http"
"github.com/TheTNB/panel/internal/service"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/systemctl"
)
type Service struct{}
func NewService() *Service {
return &Service{}
}
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
config, err := io.Read("/etc/docker/daemon.json")
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, config)
}
func (s *Service) 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("/etc/docker/daemon.json", req.Config, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = systemctl.Restart("docker"); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}
+10 -10
View File
@@ -1,29 +1,29 @@
package fail2ban
type Add struct {
Name string `json:"name"`
Type string `json:"type"`
MaxRetry int `json:"maxretry"`
FindTime int `json:"findtime"`
BanTime int `json:"bantime"`
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required"`
MaxRetry int `json:"maxretry" validate:"required"`
FindTime int `json:"findtime" validate:"required"`
BanTime int `json:"bantime" validate:"required"`
WebsiteName string `json:"website_name"`
WebsiteMode string `json:"website_mode"`
WebsitePath string `json:"website_path"`
}
type Delete struct {
Name string `json:"name"`
Name string `json:"name" validate:"required"`
}
type BanList struct {
Name string `json:"name"`
Name string `json:"name" validate:"required"`
}
type Unban struct {
Name string `json:"name"`
IP string `json:"ip"`
Name string `json:"name" validate:"required"`
IP string `json:"ip" validate:"required"`
}
type SetWhiteList struct {
IP string `json:"ip"`
IP string `json:"ip" validate:"required"`
}
+7 -4
View File
@@ -1,11 +1,13 @@
package fail2ban
import (
"fmt"
"net/http"
"regexp"
"strings"
"github.com/go-rat/chix"
"github.com/go-rat/utils/str"
"github.com/spf13/cast"
"github.com/TheTNB/panel/internal/app"
@@ -15,7 +17,6 @@ import (
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/os"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
)
type Service struct {
@@ -111,10 +112,12 @@ func (s *Service) Create(w http.ResponseWriter, r *http.Request) {
return
}
var ports string
for _, port := range website.Ports {
fields := strings.Fields(cast.ToString(port))
ports += fields[0] + ","
for _, listen := range website.Listens {
if port, err := cast.ToIntE(listen.Address); err == nil {
ports += fmt.Sprintf("%d", port) + ","
}
}
ports = strings.TrimSuffix(ports, ",")
rule := `
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START
+3 -3
View File
@@ -1,10 +1,10 @@
package frp
type Name struct {
Name string `form:"name" json:"name"`
Name string `form:"name" json:"name" validate:"required"`
}
type UpdateConfig struct {
Name string `form:"name" json:"name"`
Config string `form:"config" json:"config"`
Name string `form:"name" json:"name" validate:"required"`
Config string `form:"config" json:"config" validate:"required"`
}
+1 -1
View File
@@ -1,5 +1,5 @@
package gitea
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+3
View File
@@ -3,9 +3,12 @@ package apps
import (
"github.com/go-chi/chi/v5"
_ "github.com/TheTNB/panel/internal/apps/benchmark"
_ "github.com/TheTNB/panel/internal/apps/docker"
_ "github.com/TheTNB/panel/internal/apps/fail2ban"
_ "github.com/TheTNB/panel/internal/apps/frp"
_ "github.com/TheTNB/panel/internal/apps/gitea"
_ "github.com/TheTNB/panel/internal/apps/memcached"
_ "github.com/TheTNB/panel/internal/apps/mysql"
_ "github.com/TheTNB/panel/internal/apps/nginx"
_ "github.com/TheTNB/panel/internal/apps/php"
+20
View File
@@ -0,0 +1,20 @@
package memcached
import (
"github.com/go-chi/chi/v5"
"github.com/TheTNB/panel/pkg/apploader"
"github.com/TheTNB/panel/pkg/types"
)
func init() {
apploader.Register(&types.App{
Slug: "memcached",
Route: func(r chi.Router) {
service := NewService()
r.Get("/load", service.Load)
r.Get("/config", service.GetConfig)
r.Post("/config", service.UpdateConfig)
},
})
}
+5
View File
@@ -0,0 +1,5 @@
package memcached
type UpdateConfig struct {
Config string `form:"config" json:"config" validate:"required"`
}
+97
View File
@@ -0,0 +1,97 @@
package memcached
import (
"bufio"
"net"
"net/http"
"regexp"
"github.com/TheTNB/panel/internal/service"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/types"
)
type Service struct{}
func NewService() *Service {
return &Service{}
}
func (s *Service) 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)
return
}
if !status {
service.Success(w, []types.NV{})
return
}
conn, err := net.Dial("tcp", "127.0.0.1:11211")
if err != nil {
service.Success(w, []types.NV{})
return
}
defer conn.Close()
_, err = conn.Write([]byte("stats\nquit\n"))
if err != nil {
service.Error(w, http.StatusInternalServerError, "failed to write to Memcached: %v", err)
return
}
data := make([]types.NV, 0)
re := regexp.MustCompile(`STAT\s(\S+)\s(\S+)`)
scanner := bufio.NewScanner(conn)
for scanner.Scan() {
line := scanner.Text()
if matches := re.FindStringSubmatch(line); len(matches) == 3 {
data = append(data, types.NV{
Name: matches[1],
Value: matches[2],
})
}
if line == "END" {
break
}
}
if err = scanner.Err(); err != nil {
service.Error(w, http.StatusInternalServerError, "failed to read from Memcached: %v", err)
return
}
service.Success(w, data)
}
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
config, err := io.Read("/etc/systemd/system/memcached.service")
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, config)
}
func (s *Service) 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("/etc/systemd/system/memcached.service", req.Config, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = systemctl.Restart("memcached"); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}
+2 -2
View File
@@ -1,9 +1,9 @@
package mysql
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
type SetRootPassword struct {
Password string `form:"password" json:"password"`
Password string `form:"password" json:"password" validate:"required,password"`
}
+8 -10
View File
@@ -15,8 +15,8 @@ import (
"github.com/TheTNB/panel/pkg/db"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
)
@@ -49,13 +49,13 @@ func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
return
}
if err := io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入配置失败")
if err = io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入配置失败%v", err)
return
}
if err := systemctl.Reload("mysqld"); err != nil {
service.Error(w, http.StatusInternalServerError, "重失败")
if err = systemctl.Restart("mysqld"); err != nil {
service.Error(w, http.StatusInternalServerError, "重失败%v", err)
return
}
@@ -123,7 +123,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
if len(matches) > 1 {
d := map[string]string{"name": expression.name, "value": matches[1]}
if expression.name == "发送" || expression.name == "接收" {
d["value"] = str.FormatBytes(cast.ToFloat64(matches[1]))
d["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
}
load = append(load, d)
@@ -144,8 +144,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
// ErrorLog 获取错误日志
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 100 %s/server/mysql/mysql-error.log", app.Root)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/mysql/mysql-error.log", app.Root))
}
// ClearErrorLog 清空错误日志
@@ -160,8 +159,7 @@ func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
// SlowLog 获取慢查询日志
func (s *Service) SlowLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 100 %s/server/mysql/mysql-slow.log", app.Root)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/mysql/mysql-slow.log", app.Root))
}
// ClearSlowLog 清空慢查询日志
+1 -1
View File
@@ -1,5 +1,5 @@
package nginx
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+3 -4
View File
@@ -13,8 +13,8 @@ import (
"github.com/TheTNB/panel/internal/service"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
)
@@ -58,8 +58,7 @@ func (s *Service) SaveConfig(w http.ResponseWriter, r *http.Request) {
}
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
out, _ := shell.Execf("tail -n 100 %s/%s", app.Root, "wwwlogs/nginx-error.log")
service.Success(w, out)
service.Success(w, fmt.Sprintf("%s/%s", app.Root, "wwwlogs/nginx-error.log"))
}
func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
@@ -96,7 +95,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
service.Error(w, http.StatusInternalServerError, "获取负载失败")
return
}
mem := str.FormatBytes(cast.ToFloat64(out))
mem := tools.FormatBytes(cast.ToFloat64(out))
data = append(data, types.NV{
Name: "内存占用",
Value: mem,
+23 -72
View File
@@ -1,6 +1,8 @@
package php
import (
"fmt"
"github.com/go-chi/chi/v5"
"github.com/TheTNB/panel/pkg/apploader"
@@ -8,76 +10,25 @@ import (
)
func init() {
apploader.Register(&types.App{
Slug: "php80",
Route: func(r chi.Router) {
service := NewService(80)
r.Get("/load", service.Load)
r.Get("/config", service.GetConfig)
r.Post("/config", service.UpdateConfig)
r.Get("/fpmConfig", service.GetFPMConfig)
r.Post("/fpmConfig", service.UpdateFPMConfig)
r.Get("/errorLog", service.ErrorLog)
r.Get("/slowLog", service.SlowLog)
r.Post("/clearErrorLog", service.ClearErrorLog)
r.Post("/clearSlowLog", service.ClearSlowLog)
r.Get("/extensions", service.ExtensionList)
r.Post("/extensions", service.InstallExtension)
r.Delete("/extensions", service.UninstallExtension)
},
})
apploader.Register(&types.App{
Slug: "php81",
Route: func(r chi.Router) {
service := NewService(81)
r.Get("/load", service.Load)
r.Get("/config", service.GetConfig)
r.Post("/config", service.UpdateConfig)
r.Get("/fpmConfig", service.GetFPMConfig)
r.Post("/fpmConfig", service.UpdateFPMConfig)
r.Get("/errorLog", service.ErrorLog)
r.Get("/slowLog", service.SlowLog)
r.Post("/clearErrorLog", service.ClearErrorLog)
r.Post("/clearSlowLog", service.ClearSlowLog)
r.Get("/extensions", service.ExtensionList)
r.Post("/extensions", service.InstallExtension)
r.Delete("/extensions", service.UninstallExtension)
},
})
apploader.Register(&types.App{
Slug: "php82",
Route: func(r chi.Router) {
service := NewService(82)
r.Get("/load", service.Load)
r.Get("/config", service.GetConfig)
r.Post("/config", service.UpdateConfig)
r.Get("/fpmConfig", service.GetFPMConfig)
r.Post("/fpmConfig", service.UpdateFPMConfig)
r.Get("/errorLog", service.ErrorLog)
r.Get("/slowLog", service.SlowLog)
r.Post("/clearErrorLog", service.ClearErrorLog)
r.Post("/clearSlowLog", service.ClearSlowLog)
r.Get("/extensions", service.ExtensionList)
r.Post("/extensions", service.InstallExtension)
r.Delete("/extensions", service.UninstallExtension)
},
})
apploader.Register(&types.App{
Slug: "php83",
Route: func(r chi.Router) {
service := NewService(83)
r.Get("/load", service.Load)
r.Get("/config", service.GetConfig)
r.Post("/config", service.UpdateConfig)
r.Get("/fpmConfig", service.GetFPMConfig)
r.Post("/fpmConfig", service.UpdateFPMConfig)
r.Get("/errorLog", service.ErrorLog)
r.Get("/slowLog", service.SlowLog)
r.Post("/clearErrorLog", service.ClearErrorLog)
r.Post("/clearSlowLog", service.ClearSlowLog)
r.Get("/extensions", service.ExtensionList)
r.Post("/extensions", service.InstallExtension)
r.Delete("/extensions", service.UninstallExtension)
},
})
php := []uint{74, 80, 81, 82, 83}
for _, version := range php {
apploader.Register(&types.App{
Slug: fmt.Sprintf("php%d", version),
Route: func(r chi.Router) {
service := NewService(version)
r.Get("/load", service.Load)
r.Get("/config", service.GetConfig)
r.Post("/config", service.UpdateConfig)
r.Get("/fpmConfig", service.GetFPMConfig)
r.Post("/fpmConfig", service.UpdateFPMConfig)
r.Get("/errorLog", service.ErrorLog)
r.Get("/slowLog", service.SlowLog)
r.Post("/clearErrorLog", service.ClearErrorLog)
r.Post("/clearSlowLog", service.ClearSlowLog)
r.Get("/extensions", service.ExtensionList)
r.Post("/extensions", service.InstallExtension)
r.Delete("/extensions", service.UninstallExtension)
},
})
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
package php
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
type ExtensionSlug struct {
Slug string `form:"slug" json:"slug"`
Slug string `form:"slug" json:"slug" validate:"required"`
}
+47 -56
View File
@@ -87,7 +87,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
client := resty.New().SetTimeout(10 * time.Second)
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1/phpfpm_status/%d", s.version))
if err != nil || !resp.IsSuccess() {
service.Error(w, http.StatusInternalServerError, "获取负载状态失败:%v", err)
service.Success(w, []types.NV{})
return
}
@@ -111,13 +111,11 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
}
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 500 %s/server/php/%d/var/log/php-fpm.log", app.Root, s.version)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/php/%d/var/log/php-fpm.log", app.Root, s.version))
}
func (s *Service) SlowLog(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 500 %s/server/php/%d/var/log/slow.log", app.Root, s.version)
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/php/%d/var/log/slow.log", app.Root, s.version))
}
func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
@@ -140,17 +138,6 @@ func (s *Service) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
func (s *Service) ExtensionList(w http.ResponseWriter, r *http.Request) {
extensions := s.getExtensions()
// ionCube 只支持 PHP 8.3 以下版本
if cast.ToUint(s.version) < 83 {
extensions = append(extensions, Extension{
Name: "ionCube",
Slug: "ionCube Loader",
Description: "ionCube 是一个专业级的 PHP 加密解密工具。",
Installed: false,
})
}
raw, err := shell.Execf("%s/server/php/%d/bin/php -m", app.Root, s.version)
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
@@ -184,10 +171,10 @@ func (s *Service) InstallExtension(w http.ResponseWriter, r *http.Request) {
return
}
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s.sh" | bash -s -- "install" "%d" >> /tmp/%s.log 2>&1`, url.QueryEscape(req.Slug), s.version, req.Slug)
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/%s.sh' | bash -s -- 'install' '%d' >> '/tmp/%s.log' 2>&1`, url.PathEscape(req.Slug), s.version, req.Slug)
officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"}
if slices.Contains(officials, req.Slug) {
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/official.sh" | bash -s -- "install" "%d" "%s" >> /tmp/%s.log 2>&1`, s.version, req.Slug, req.Slug)
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/official.sh' | bash -s -- 'install' '%d' '%s' >> '/tmp/%s.log' 2>&1`, s.version, req.Slug, req.Slug)
}
task := new(biz.Task)
@@ -215,10 +202,10 @@ func (s *Service) UninstallExtension(w http.ResponseWriter, r *http.Request) {
return
}
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s.sh" | bash -s -- "uninstall" "%d" >> /tmp/%s.log 2>&1`, url.QueryEscape(req.Slug), s.version, req.Slug)
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/%s.sh' | bash -s -- 'uninstall' '%d' >> '/tmp/%s.log' 2>&1`, url.PathEscape(req.Slug), s.version, req.Slug)
officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"}
if slices.Contains(officials, req.Slug) {
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/official.sh" | bash -s -- "uninstall" "%d" "%s" >> /tmp/%s.log 2>&1`, s.version, req.Slug, req.Slug)
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/official.sh' | bash -s -- 'uninstall' '%d' '%s' >> '/tmp/%s.log' 2>&1`, s.version, req.Slug, req.Slug)
}
task := new(biz.Task)
@@ -239,137 +226,141 @@ func (s *Service) getExtensions() []Extension {
{
Name: "fileinfo",
Slug: "fileinfo",
Description: "Fileinfo 是一个用于识别文件类型的库",
Description: "Fileinfo 是一个用于识别文件类型的库",
},
{
Name: "OPcache",
Slug: "Zend OPcache",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销",
},
{
Name: "PhpRedis",
Name: "Redis",
Slug: "redis",
Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据",
Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据",
},
{
Name: "Memcached",
Slug: "memcached",
Description: "Memcached 使用 libmemcached 库连接 Memcached 服务器",
},
{
Name: "ImageMagick",
Slug: "imagick",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件",
},
{
Name: "exif",
Slug: "exif",
Description: "通过 exif 扩展,可以操作图像元数据",
Description: "通过 exif 扩展,可以操作图像元数据",
},
{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: "(需先安装PostgreSQLpdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问",
Description: "pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问(需先安装PostgreSQL",
},
{
Name: "imap",
Slug: "imap",
Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件",
Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件",
},
{
Name: "zip",
Slug: "zip",
Description: "Zip 是一个用于处理 ZIP 文件的库",
Description: "Zip 是一个用于处理 ZIP 文件的库",
},
{
Name: "bz2",
Slug: "bz2",
Description: "Bzip2 是一个用于压缩和解压缩文件的库",
Description: "Bzip2 是一个用于压缩和解压缩文件的库",
},
{
Name: "readline",
Slug: "readline",
Description: "Readline 是一个库,它提供了一种用于处理文本的接口",
Description: "Readline 是一个库,它提供了一种用于处理文本的接口",
},
{
Name: "snmp",
Slug: "snmp",
Description: "SNMP 是一种用于网络管理的协议",
Description: "SNMP 是一种用于网络管理的协议",
},
{
Name: "ldap",
Slug: "ldap",
Description: "LDAP 是一种用于访问目录服务的协议",
Description: "LDAP 是一种用于访问目录服务的协议",
},
{
Name: "enchant",
Slug: "enchant",
Description: "Enchant 是一个拼写检查库",
Description: "Enchant 是一个拼写检查库",
},
{
Name: "pspell",
Slug: "pspell",
Description: "Pspell 是一个拼写检查库",
Description: "Pspell 是一个拼写检查库",
},
{
Name: "calendar",
Slug: "calendar",
Description: "Calendar 是一个用于处理日期的库",
Description: "Calendar 是一个用于处理日期的库",
},
{
Name: "gmp",
Slug: "gmp",
Description: "GMP 是一个用于处理大整数的库",
Description: "GMP 是一个用于处理大整数的库",
},
{
Name: "sysvmsg",
Slug: "sysvmsg",
Description: "Sysvmsg 是一个用于处理 System V 消息队列的库",
Description: "Sysvmsg 是一个用于处理 System V 消息队列的库",
},
{
Name: "sysvsem",
Slug: "sysvsem",
Description: "Sysvsem 是一个用于处理 System V 信号量的库",
Description: "Sysvsem 是一个用于处理 System V 信号量的库",
},
{
Name: "sysvshm",
Slug: "sysvshm",
Description: "Sysvshm 是一个用于处理 System V 共享内存的库",
Description: "Sysvshm 是一个用于处理 System V 共享内存的库",
},
{
Name: "xsl",
Slug: "xsl",
Description: "XSL 是一个用于处理 XML 文档的库",
Description: "XSL 是一个用于处理 XML 文档的库",
},
{
Name: "intl",
Slug: "intl",
Description: "Intl 是一个用于处理国际化和本地化的库",
Description: "Intl 是一个用于处理国际化和本地化的库",
},
{
Name: "gettext",
Slug: "gettext",
Description: "Gettext 是一个用于处理多语言的库",
Description: "Gettext 是一个用于处理多语言的库",
},
{
Name: "igbinary",
Slug: "igbinary",
Description: "Igbinary 是一个用于序列化和反序列化数据的库",
Description: "Igbinary 是一个用于序列化和反序列化数据的库",
},
{
Name: "ionCube",
Slug: "ionCube Loader",
Description: "ionCube 是一个专业级的 PHP 加密解密工具(需在 OPcache 之后安装)",
},
{
Name: "Swoole",
Slug: "swoole",
Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展",
},
{
Name: "Swow",
Slug: "Swow",
Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展。",
Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展",
},
}
// ionCube 只支持 PHP 8.3 以下版本
if cast.ToUint(s.version) < 83 {
// Swow 不支持 PHP 8.0 以下版本
if cast.ToUint(s.version) >= 80 {
extensions = append(extensions, Extension{
Name: "ionCube",
Slug: "ionCube Loader",
Description: "ionCube 是一个专业级的 PHP 加密解密工具。",
Installed: false,
Name: "Swow",
Slug: "Swow",
Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展。",
})
}
+2 -2
View File
@@ -1,9 +1,9 @@
package phpmyadmin
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
type UpdatePort struct {
Port uint `form:"port" json:"port"`
Port uint `form:"port" json:"port" validate:"required,number,gte=1,lte=65535"`
}
+5 -2
View File
@@ -78,8 +78,11 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
fw := firewall.NewFirewall()
err = fw.Port(firewall.FireInfo{
Port: req.Port,
Protocol: "tcp",
Type: firewall.TypeNormal,
PortStart: req.Port,
PortEnd: req.Port,
Direction: firewall.DirectionIn,
Strategy: firewall.StrategyAccept,
}, firewall.OperationAdd)
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
+1 -1
View File
@@ -1,5 +1,5 @@
package podman
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+1 -1
View File
@@ -1,5 +1,5 @@
package postgresql
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+1 -2
View File
@@ -132,8 +132,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
// Log 获取日志
func (s *Service) Log(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf("tail -n 100 %s/server/postgresql/logs/postgresql-%s.log", app.Root, time.Now().Format(time.DateOnly))
service.Success(w, log)
service.Success(w, fmt.Sprintf("%s/server/postgresql/logs/postgresql-%s.log", app.Root, time.Now().Format(time.DateOnly)))
}
// ClearLog 清空日志
+7 -7
View File
@@ -1,20 +1,20 @@
package pureftpd
type Create struct {
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
Path string `form:"path" json:"path"`
Username string `form:"username" json:"username" validate:"required"`
Password string `form:"password" json:"password" validate:"required,password"`
Path string `form:"path" json:"path" validate:"required"`
}
type Delete struct {
Username string `form:"username" json:"username"`
Username string `form:"username" json:"username" validate:"required"`
}
type ChangePassword struct {
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
Username string `form:"username" json:"username" validate:"required"`
Password string `form:"password" json:"password" validate:"required,password"`
}
type UpdatePort struct {
Port uint `form:"port" json:"port"`
Port uint `form:"port" json:"port" validate:"required,number,gte=1,lte=65535"`
}
+5 -2
View File
@@ -156,8 +156,11 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
fw := firewall.NewFirewall()
err = fw.Port(firewall.FireInfo{
Port: req.Port,
Protocol: "tcp",
Type: firewall.TypeNormal,
PortStart: req.Port,
PortEnd: req.Port,
Direction: firewall.DirectionIn,
Strategy: firewall.StrategyAccept,
}, firewall.OperationAdd)
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
+1 -1
View File
@@ -1,5 +1,5 @@
package redis
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+10 -10
View File
@@ -1,27 +1,27 @@
package rsync
type Create struct {
Name string `form:"name" json:"name"`
Path string `form:"path" json:"path"`
Name string `form:"name" json:"name" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
Comment string `form:"comment" json:"comment"`
AuthUser string `form:"auth_user" json:"auth_user"`
Secret string `form:"secret" json:"secret"`
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
Secret string `form:"secret" json:"secret" validate:"required"`
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
}
type Delete struct {
Name string `form:"name" json:"name"`
Name string `form:"name" json:"name" validate:"required"`
}
type Update struct {
Name string `form:"name" json:"name"`
Path string `form:"path" json:"path"`
Name string `form:"name" json:"name" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
Comment string `form:"comment" json:"comment"`
AuthUser string `form:"auth_user" json:"auth_user"`
Secret string `form:"secret" json:"secret"`
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
Secret string `form:"secret" json:"secret" validate:"required"`
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
}
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
+1 -1
View File
@@ -7,11 +7,11 @@ import (
"strings"
"github.com/go-rat/chix"
"github.com/go-rat/utils/str"
"github.com/TheTNB/panel/internal/service"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/systemctl"
)
+6 -6
View File
@@ -1,13 +1,13 @@
package s3fs
type Create struct {
Ak string `form:"ak" json:"ak"`
Sk string `form:"sk" json:"sk"`
Bucket string `form:"bucket" json:"bucket"`
URL string `form:"url" json:"url"`
Path string `form:"path" json:"path"`
Ak string `form:"ak" json:"ak" validate:"required"`
Sk string `form:"sk" json:"sk" validate:"required"`
Bucket string `form:"bucket" json:"bucket" validate:"required"`
URL string `form:"url" json:"url" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
}
type Delete struct {
ID int64 `form:"id" json:"id"`
ID int64 `form:"id" json:"id" validate:"required"`
}
-1
View File
@@ -13,7 +13,6 @@ func init() {
Route: func(r chi.Router) {
service := NewService()
r.Get("/service", service.Service)
r.Get("/log", service.Log)
r.Post("/clearLog", service.ClearLog)
r.Get("/config", service.GetConfig)
r.Post("/config", service.UpdateConfig)
+9 -9
View File
@@ -1,22 +1,22 @@
package supervisor
type UpdateConfig struct {
Config string `form:"config" json:"config"`
Config string `form:"config" json:"config" validate:"required"`
}
type UpdateProcessConfig struct {
Process string `form:"process" json:"process"`
Config string `form:"config" json:"config"`
Process string `form:"process" json:"process" validate:"required"`
Config string `form:"config" json:"config" validate:"required"`
}
type ProcessName struct {
Process string `form:"process" json:"process"`
Process string `form:"process" json:"process" validate:"required"`
}
type CreateProcess struct {
Name string `form:"name" json:"name"`
User string `form:"user" json:"user"`
Path string `form:"path" json:"path"`
Command string `form:"command" json:"command"`
Num int `form:"num" json:"num"`
Name string `form:"name" json:"name" validate:"required"`
User string `form:"user" json:"user" validate:"required"`
Path string `form:"path" json:"path" validate:"required"`
Command string `form:"command" json:"command" validate:"required"`
Num int `form:"num" json:"num" validate:"required,gte=1"`
}
+1 -8
View File
@@ -36,12 +36,6 @@ func (s *Service) Service(w http.ResponseWriter, r *http.Request) {
service.Success(w, s.name)
}
// Log 日志
func (s *Service) Log(w http.ResponseWriter, r *http.Request) {
log, _ := shell.Execf(`tail -n 200 /var/log/supervisor/supervisord.log`)
service.Success(w, log)
}
// ClearLog 清空日志
func (s *Service) ClearLog(w http.ResponseWriter, r *http.Request) {
if _, err := shell.Execf(`echo "" > /var/log/supervisor/supervisord.log`); err != nil {
@@ -204,8 +198,7 @@ func (s *Service) ProcessLog(w http.ResponseWriter, r *http.Request) {
return
}
log, _ := shell.Execf(`tail -n 200 '%s'`, logPath)
service.Success(w, log)
service.Success(w, logPath)
}
// ClearProcessLog 清空进程日志
+4
View File
@@ -18,6 +18,10 @@ func init() {
r.Post("/swap", service.UpdateSWAP)
r.Get("/timezone", service.GetTimezone)
r.Post("/timezone", service.UpdateTimezone)
r.Post("/time", service.UpdateTime)
r.Post("/syncTime", service.SyncTime)
r.Get("/hostname", service.GetHostname)
r.Post("/hostname", service.UpdateHostname)
r.Get("/hosts", service.GetHosts)
r.Post("/hosts", service.UpdateHosts)
r.Post("/rootPassword", service.UpdateRootPassword)
+15 -5
View File
@@ -1,16 +1,26 @@
package toolbox
import "time"
type DNS struct {
DNS1 string `form:"dns1" json:"dns1"`
DNS2 string `form:"dns2" json:"dns2"`
DNS1 string `form:"dns1" json:"dns1" validate:"required"`
DNS2 string `form:"dns2" json:"dns2" validate:"required"`
}
type SWAP struct {
Size int64 `form:"size" json:"size"`
Size int64 `form:"size" json:"size" validate:"gte=0"`
}
type Timezone struct {
Timezone string `form:"timezone" json:"timezone"`
Timezone string `form:"timezone" json:"timezone" validate:"required"`
}
type Time struct {
Time time.Time `form:"time" json:"time" validate:"required"`
}
type Hostname struct {
Hostname string `form:"hostname" json:"hostname" validate:"required,regex=^[a-zA-Z0-9][a-zA-Z0-9-]{061}[a-zA-Z0-9]$"`
}
type Hosts struct {
@@ -18,5 +28,5 @@ type Hosts struct {
}
type Password struct {
Password string `form:"password" json:"password"`
Password string `form:"password" json:"password" validate:"required,password"`
}
+64 -10
View File
@@ -12,8 +12,9 @@ import (
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/service"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/ntp"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
)
@@ -76,7 +77,7 @@ func (s *Service) GetSWAP(w http.ResponseWriter, r *http.Request) {
}
size = file.Size() / 1024 / 1024
total = str.FormatBytes(float64(file.Size()))
total = tools.FormatBytes(float64(file.Size()))
} else {
size = 0
total = "0.00 B"
@@ -90,8 +91,8 @@ func (s *Service) GetSWAP(w http.ResponseWriter, r *http.Request) {
match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
if len(match) >= 4 {
used = str.FormatBytes(cast.ToFloat64(match[2]) * 1024)
free = str.FormatBytes(cast.ToFloat64(match[3]) * 1024)
used = tools.FormatBytes(cast.ToFloat64(match[2]) * 1024)
free = tools.FormatBytes(cast.ToFloat64(match[3]) * 1024)
}
service.Success(w, chix.M{
@@ -133,7 +134,7 @@ func (s *Service) UpdateSWAP(w http.ResponseWriter, r *http.Request) {
return
}
if cast.ToInt64(free)*1024 < req.Size*1024*1024 {
service.Error(w, http.StatusInternalServerError, "硬盘空间不足,当前剩余%s", str.FormatBytes(cast.ToFloat64(free)))
service.Error(w, http.StatusInternalServerError, "硬盘空间不足,当前剩余%s", tools.FormatBytes(cast.ToFloat64(free)))
return
}
@@ -221,6 +222,64 @@ func (s *Service) UpdateTimezone(w http.ResponseWriter, r *http.Request) {
service.Success(w, nil)
}
// UpdateTime 设置时间
func (s *Service) UpdateTime(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Time](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if err = ntp.UpdateSystemTime(req.Time); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}
// SyncTime 同步时间
func (s *Service) SyncTime(w http.ResponseWriter, r *http.Request) {
now, err := ntp.Now()
if err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
if err = ntp.UpdateSystemTime(now); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
return
}
service.Success(w, nil)
}
// GetHostname 获取主机名
func (s *Service) GetHostname(w http.ResponseWriter, r *http.Request) {
hostname, _ := io.Read("/etc/hostname")
service.Success(w, strings.TrimSpace(hostname))
}
// UpdateHostname 设置主机名
func (s *Service) UpdateHostname(w http.ResponseWriter, r *http.Request) {
req, err := service.Bind[Hostname](r)
if err != nil {
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
return
}
if _, err = shell.Execf("hostnamectl set-hostname '%s'", req.Hostname); err != nil {
// 直接写 /etc/hostname
if err = io.Write("/etc/hostname", req.Hostname, 0644); err != nil {
service.Error(w, http.StatusInternalServerError, "写入主机名失败")
return
}
}
service.Success(w, nil)
}
// GetHosts 获取 hosts 信息
func (s *Service) GetHosts(w http.ResponseWriter, r *http.Request) {
hosts, _ := io.Read("/etc/hosts")
@@ -251,11 +310,6 @@ func (s *Service) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
return
}
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(req.Password) {
service.Error(w, http.StatusUnprocessableEntity, "密码必须为 6-20 位字母、数字或特殊字符")
return
}
req.Password = strings.ReplaceAll(req.Password, `'`, `\'`)
if _, err = shell.Execf(`yes '%s' | passwd root`, req.Password); err != nil {
service.Error(w, http.StatusInternalServerError, "%v", err)
+6 -2
View File
@@ -5,6 +5,7 @@ import (
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/acme"
"github.com/TheTNB/panel/pkg/types"
)
type Cert struct {
@@ -12,7 +13,7 @@ type Cert struct {
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, 4096)
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 (续签时使用)
@@ -27,13 +28,16 @@ type Cert struct {
}
type CertRepo interface {
List(page, limit uint) ([]*Cert, int64, error)
List(page, limit uint) ([]*types.CertList, int64, error)
Get(id uint) (*Cert, error)
GetByWebsite(WebsiteID uint) (*Cert, error)
Upload(req *request.CertUpload) (*Cert, error)
Create(req *request.CertCreate) (*Cert, error)
Update(req *request.CertUpdate) error
Delete(id uint) error
ObtainAuto(id uint) (*acme.Certificate, error)
ObtainManual(id uint) (*acme.Certificate, error)
ObtainSelfSigned(id uint) error
Renew(id uint) (*acme.Certificate, error)
ManualDNS(id uint) ([]acme.DNSRecord, error)
Deploy(ID, WebsiteID uint) error
+2 -1
View File
@@ -13,7 +13,7 @@ type CertAccount struct {
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"`
KeyType string `gorm:"not null" json:"key_type"` // 密钥类型 (P256, P384, 2048, 3072, 4096)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -22,6 +22,7 @@ type CertAccount struct {
type CertAccountRepo interface {
List(page, limit uint) ([]*CertAccount, int64, error)
GetDefault(userID uint) (*CertAccount, error)
Get(id uint) (*CertAccount, error)
Create(req *request.CertAccountCreate) (*CertAccount, error)
Update(req *request.CertAccountUpdate) error
+2 -8
View File
@@ -1,15 +1,13 @@
package biz
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/types"
)
type ContainerRepo interface {
ListAll() ([]types.Container, error)
ListByNames(names []string) ([]types.Container, error)
ListByName(name string) ([]types.Container, error)
Create(req *request.ContainerCreate) (string, error)
Remove(id string) error
Start(id string) error
@@ -17,12 +15,8 @@ type ContainerRepo interface {
Restart(id string) error
Pause(id string) error
Unpause(id string) error
Inspect(id string) (types.ContainerJSON, error)
Kill(id string) error
Rename(id string, newName string) error
Stats(id string) (container.StatsResponseReader, error)
Exist(name string) (bool, error)
Update(id string, config container.UpdateConfig) error
Logs(id string) (string, error)
Prune() error
}
+2 -6
View File
@@ -1,17 +1,13 @@
package biz
import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/types"
)
type ContainerImageRepo interface {
List() ([]image.Summary, error)
Exist(id string) (bool, error)
List() ([]types.ContainerImage, error)
Pull(req *request.ContainerImagePull) error
Remove(id string) error
Prune() error
Inspect(id string) (types.ImageInspect, error)
}
+2 -7
View File
@@ -1,18 +1,13 @@
package biz
import (
"github.com/docker/docker/api/types/network"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/types"
)
type ContainerNetworkRepo interface {
List() ([]network.Inspect, error)
List() ([]types.ContainerNetwork, error)
Create(req *request.ContainerNetworkCreate) (string, error)
Remove(id string) error
Exist(name string) (bool, error)
Inspect(id string) (network.Inspect, error)
Connect(networkID string, containerID string) error
Disconnect(networkID string, containerID string) error
Prune() error
}
+3 -6
View File
@@ -1,16 +1,13 @@
package biz
import (
"github.com/docker/docker/api/types/volume"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/types"
)
type ContainerVolumeRepo interface {
List() ([]*volume.Volume, error)
Create(req *request.ContainerVolumeCreate) (volume.Volume, error)
Exist(name string) (bool, error)
Inspect(id string) (volume.Volume, error)
List() ([]types.ContainerVolume, error)
Create(req *request.ContainerVolumeCreate) (string, error)
Remove(id string) error
Prune() error
}
-1
View File
@@ -26,5 +26,4 @@ type CronRepo interface {
Update(req *request.CronUpdate) error
Delete(id uint) error
Status(id uint, status bool) error
Log(id uint) (string, error)
}
+64 -11
View File
@@ -1,16 +1,69 @@
package biz
import "time"
import (
"time"
"github.com/go-rat/utils/crypt"
"gorm.io/gorm"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/http/request"
)
type DatabaseStatus string
const (
DatabaseStatusNormal DatabaseStatus = "normal"
DatabaseStatusInvalid DatabaseStatus = "invalid"
)
type Database struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"not null;unique" json:"name"`
Type string `gorm:"not null" json:"type"`
Host string `gorm:"not null" json:"host"`
Port int `gorm:"not null" json:"port"`
Username string `gorm:"not null" json:"username"`
Password string `gorm:"not null" json:"password"`
Remark string `gorm:"not null" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint `gorm:"primaryKey" json:"id"`
ServerID uint `gorm:"not null" json:"server_id"`
Name string `gorm:"not null" json:"name"`
Status DatabaseStatus `gorm:"not null" json:"status"`
Username string `gorm:"not null" json:"username"`
Password string `gorm:"not null" json:"password"`
Remark string `gorm:"not null" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Server *DatabaseServer `gorm:"foreignKey:ServerID" json:"server"`
}
func (r *Database) BeforeSave(tx *gorm.DB) error {
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
if err != nil {
return err
}
r.Password, err = crypter.Encrypt([]byte(r.Password))
if err != nil {
return err
}
return nil
}
func (r *Database) AfterFind(tx *gorm.DB) error {
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
if err != nil {
return err
}
password, err := crypter.Decrypt(r.Password)
if err == nil {
r.Password = string(password)
}
return nil
}
type DatabaseRepo interface {
Count() (int64, error)
List(page, limit uint) ([]*Database, int64, error)
Get(id uint) (*Database, error)
Create(req *request.DatabaseCreate) error
Update(req *request.DatabaseUpdate) error
Delete(id uint) error
}
+72
View File
@@ -0,0 +1,72 @@
package biz
import (
"time"
"github.com/go-rat/utils/crypt"
"gorm.io/gorm"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/http/request"
)
type DatabaseType string
const (
DatabaseTypeMysql DatabaseType = "mysql"
DatabaseTypePostgresql DatabaseType = "postgresql"
DatabaseTypeRedis DatabaseType = "redis"
)
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"`
Remark string `gorm:"not null" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Databases []*Database `gorm:"foreignKey:ServerID" json:"-"`
}
func (r *DatabaseServer) BeforeSave(tx *gorm.DB) error {
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
if err != nil {
return err
}
r.Password, err = crypter.Encrypt([]byte(r.Password))
if err != nil {
return err
}
return nil
}
func (r *DatabaseServer) AfterFind(tx *gorm.DB) error {
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
if err != nil {
return err
}
password, err := crypter.Decrypt(r.Password)
if err == nil {
r.Password = string(password)
}
return nil
}
type DatabaseServerRepo interface {
Count() (int64, error)
List(page, limit uint) ([]*DatabaseServer, int64, error)
Get(id uint) (*DatabaseServer, error)
Create(req *request.DatabaseServerCreate) error
Update(req *request.DatabaseServerCreate) error
Delete(id uint) error
}
-4
View File
@@ -1,4 +0,0 @@
package biz
type FirewallRepo interface {
}
+5 -5
View File
@@ -4,14 +4,14 @@ import (
"time"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
)
type Monitor struct {
ID uint `gorm:"primaryKey" json:"id"`
Info tools.MonitoringInfo `gorm:"not null;serializer:json" json:"info"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint `gorm:"primaryKey" json:"id"`
Info types.CurrentInfo `gorm:"not null;serializer:json" json:"info"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type MonitorRepo interface {
+3 -4
View File
@@ -17,10 +17,7 @@ const (
SettingKeyBackupPath SettingKey = "backup_path"
SettingKeyWebsitePath SettingKey = "website_path"
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
SettingKeySshHost SettingKey = "ssh_host"
SettingKeySshPort SettingKey = "ssh_port"
SettingKeySshUser SettingKey = "ssh_user"
SettingKeySshPassword SettingKey = "ssh_password"
SettingKeyOfflineMode SettingKey = "offline_mode"
)
type Setting struct {
@@ -33,9 +30,11 @@ type Setting struct {
type SettingRepo interface {
Get(key SettingKey, defaultValue ...string) (string, error)
GetBool(key SettingKey, defaultValue ...bool) (bool, error)
Set(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)
UpdatePanel(version, url, checksum string) error
FixPanel() error
}
+63 -3
View File
@@ -1,8 +1,68 @@
package biz
import "github.com/TheTNB/panel/internal/http/request"
import (
"time"
"github.com/go-rat/utils/crypt"
"gorm.io/gorm"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/ssh"
)
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"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (r *SSH) BeforeSave(tx *gorm.DB) error {
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
if err != nil {
return err
}
r.Config.Key, err = crypter.Encrypt([]byte(r.Config.Key))
if err != nil {
return err
}
r.Config.Password, err = crypter.Encrypt([]byte(r.Config.Password))
if err != nil {
return err
}
return nil
}
func (r *SSH) AfterFind(tx *gorm.DB) error {
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
if err != nil {
return err
}
key, err := crypter.Decrypt(r.Config.Key)
if err == nil {
r.Config.Key = string(key)
}
password, err := crypter.Decrypt(r.Config.Password)
if err == nil {
r.Config.Password = string(password)
}
return nil
}
type SSHRepo interface {
GetInfo() (map[string]any, error)
UpdateInfo(req *request.SSHUpdateInfo) error
List(page, limit uint) ([]*SSH, int64, error)
Get(id uint) (*SSH, error)
Create(req *request.SSHCreate) error
Update(req *request.SSHUpdate) error
Delete(id uint) error
}
+1 -1
View File
@@ -28,5 +28,5 @@ type TaskRepo interface {
Delete(id uint) error
UpdateStatus(id uint, status TaskStatus) error
Push(task *Task) error
DispatchWaiting() error
DispatchWaiting()
}
+3 -2
View File
@@ -1,6 +1,7 @@
package biz
import (
"context"
"time"
"github.com/TheTNB/panel/internal/http/request"
@@ -12,8 +13,7 @@ type Website struct {
Name string `gorm:"not null;unique" json:"name"`
Status bool `gorm:"not null;default:true" json:"status"`
Path string `gorm:"not null" json:"path"`
PHP int `gorm:"not null" json:"php"`
SSL bool `gorm:"not null" json:"ssl"`
Https bool `gorm:"not null" json:"https"`
Remark string `gorm:"not null" json:"remark"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -34,4 +34,5 @@ type WebsiteRepo interface {
UpdateRemark(id uint, remark string) error
ResetConfig(id uint) error
UpdateStatus(id uint, status bool) error
ObtainCert(ctx context.Context, id uint) error
}
+6 -1
View File
@@ -1,10 +1,15 @@
package bootstrap
import (
"os"
"runtime/debug"
)
func boot() {
if os.Geteuid() != 0 {
panic("panel must run as root")
}
debug.SetGCPercent(10)
debug.SetMemoryLimit(64 << 20)
@@ -19,8 +24,8 @@ func BootWeb() {
boot()
initValidator()
initSession()
initCron()
initQueue()
initCron()
initHttp()
select {}
+4 -2
View File
@@ -2,10 +2,10 @@ package bootstrap
import (
"context"
"fmt"
"os"
"strings"
"github.com/gookit/color"
"github.com/urfave/cli/v3"
"github.com/TheTNB/panel/internal/app"
@@ -13,6 +13,8 @@ import (
)
func initCli() {
app.IsCli = true
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "NAME", "名称")
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "USAGE", "用法")
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "VERSION", "版本")
@@ -43,6 +45,6 @@ func initCli() {
Commands: route.Cli(),
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
color.Redln(err.Error())
fmt.Printf("|-%v\n", err)
}
}
+6 -1
View File
@@ -25,8 +25,13 @@ func initConf() {
}
func initGlobal() {
app.Key = app.Conf.MustString("app.key")
if len(app.Key) != 32 {
log.Fatalf("app key must be 32 characters")
}
app.Root = app.Conf.MustString("app.root")
app.Version = "2.3.3"
app.Version = "2.3.18"
app.Locale = app.Conf.MustString("app.locale")
// 初始化时区
+2 -11
View File
@@ -6,25 +6,16 @@ import (
"github.com/glebarez/sqlite"
"github.com/go-gormigrate/gormigrate/v2"
slogGorm "github.com/orandin/slog-gorm"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"moul.io/zapgorm2"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/migration"
)
func initOrm() {
logLevel := logger.Error
if app.Conf.Bool("database.debug") {
logLevel = logger.Info
}
zapLogger := zapgorm2.New(app.Logger)
zapLogger.LogMode(logLevel)
zapLogger.SetAsDefault()
db, err := gorm.Open(sqlite.Open(filepath.Join(app.Root, "panel/storage/app.db")), &gorm.Config{
Logger: zapLogger,
Logger: slogGorm.New(slogGorm.WithHandler(app.Logger.Handler())),
SkipDefaultTransaction: true,
DisableForeignKeyConstraintWhenMigrating: true,
})
+8 -3
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"path/filepath"
"github.com/bddjr/hlfhr"
"github.com/go-chi/chi/v5"
"github.com/TheTNB/panel/internal/app"
@@ -23,17 +24,21 @@ func initHttp() {
// add route
route.Http(app.Http)
route.Ws(app.Http)
apps.Boot(app.Http)
srv := &http.Server{
srv := hlfhr.New(&http.Server{
Addr: fmt.Sprintf(":%d", app.Conf.MustInt("http.port")),
Handler: http.AllowQuerySemicolons(app.Http),
MaxHeaderBytes: 2048 << 20,
}
})
srv.HttpOnHttpsPortErrorHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
hlfhr.RedirectToHttps(w, r, http.StatusTemporaryRedirect)
})
if app.Conf.Bool("http.tls") {
srv.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
MinVersion: tls.VersionTLS13,
}
cert := filepath.Join(app.Root, "panel/storage/cert.pem")
+11 -11
View File
@@ -1,10 +1,12 @@
package bootstrap
import (
"log/slog"
"path/filepath"
"time"
"go.uber.org/zap"
"go.uber.org/zap/exp/zapslog"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
@@ -13,16 +15,15 @@ import (
func initLogger() {
writeSyncer := zapcore.AddSync(&lumberjack.Logger{
Filename: filepath.Join(app.Root, "panel/storage/logs/app.log"),
MaxSize: 10,
MaxBackups: 10,
MaxAge: 30,
Compress: true,
Filename: filepath.Join(app.Root, "panel/storage/logs/app.log"),
MaxSize: 10,
MaxAge: 30,
Compress: true,
})
level := zapcore.InfoLevel
level := zap.InfoLevel
if app.Conf.Bool("app.debug") {
level = zapcore.DebugLevel
level = zap.DebugLevel
}
config := zap.NewProductionEncoderConfig()
@@ -34,8 +35,7 @@ func initLogger() {
)
logger := zap.New(core)
defer func(logger *zap.Logger) {
_ = logger.Sync()
}(logger)
app.Logger = logger
zap.ReplaceGlobals(logger)
app.Logger = slog.New(zapslog.NewHandler(logger.Core()))
slog.SetDefault(app.Logger)
}
+1 -1
View File
@@ -8,6 +8,6 @@ import (
)
func initQueue() {
app.Queue = queue.New(40)
app.Queue = queue.New(100)
go app.Queue.Run(context.Background())
}
+1 -1
View File
@@ -12,7 +12,7 @@ import (
func initSession() {
// initialize session manager
manager, err := sessions.NewManager(&sessions.ManagerOptions{
Key: app.Conf.String("app.key"),
Key: app.Key,
Lifetime: 120,
GcInterval: 30,
DisableDefaultDriver: true,
+4
View File
@@ -9,6 +9,7 @@ import (
"github.com/go-playground/validator/v10/translations/zh"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/http/rule"
)
func initValidator() {
@@ -23,4 +24,7 @@ func initValidator() {
app.Translator = &trans
app.Validator = validate
if err := rule.RegisterRules(validate); err != nil {
log.Fatalf("failed to register validator rules: %v", err)
}
}
+20 -16
View File
@@ -7,6 +7,7 @@ import (
"slices"
"github.com/expr-lang/expr"
"github.com/go-rat/utils/collect"
"github.com/hashicorp/go-version"
"github.com/spf13/cast"
@@ -14,7 +15,7 @@ import (
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/pkg/api"
"github.com/TheTNB/panel/pkg/apploader"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/shell"
)
type appRepo struct {
@@ -64,7 +65,7 @@ func (r *appRepo) UpdateExist(slug string) bool {
for channel := range slices.Values(item.Channels) {
if channel.Slug == installed.Channel {
current := str.FirstElement(channel.Subs)
current := collect.First(channel.Subs)
if current != nil && current.Version != installed.Version {
return true
}
@@ -167,7 +168,7 @@ func (r *appRepo) Install(channel, slug string) error {
}
shellUrl = ch.Install
shellChannel = ch.Slug
shellVersion = str.FirstElement(ch.Subs).Version
shellVersion = collect.First(ch.Subs).Version
break
}
}
@@ -179,16 +180,17 @@ func (r *appRepo) Install(channel, slug string) error {
return err
}
if app.IsCli {
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
}
task := new(biz.Task)
task.Name = "安装应用 " + 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"
if err = r.taskRepo.Push(task); err != nil {
return err
}
return err
return r.taskRepo.Push(task)
}
func (r *appRepo) UnInstall(slug string) error {
@@ -233,16 +235,17 @@ func (r *appRepo) UnInstall(slug string) error {
return err
}
if app.IsCli {
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
}
task := new(biz.Task)
task.Name = "卸载应用 " + 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"
if err = r.taskRepo.Push(task); err != nil {
return err
}
return err
return r.taskRepo.Push(task)
}
func (r *appRepo) Update(slug string) error {
@@ -275,7 +278,7 @@ func (r *appRepo) Update(slug string) error {
}
shellUrl = ch.Update
shellChannel = ch.Slug
shellVersion = str.FirstElement(ch.Subs).Version
shellVersion = collect.First(ch.Subs).Version
break
}
}
@@ -287,16 +290,17 @@ func (r *appRepo) Update(slug string) error {
return err
}
if app.IsCli {
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
}
task := new(biz.Task)
task.Name = "更新应用 " + 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"
if err = r.taskRepo.Push(task); err != nil {
return err
}
return err
return r.taskRepo.Push(task)
}
func (r *appRepo) UpdateShow(slug string, show bool) error {
+69 -51
View File
@@ -9,7 +9,6 @@ import (
"strings"
"time"
"github.com/gookit/color"
"github.com/shirou/gopsutil/disk"
"github.com/TheTNB/panel/internal/app"
@@ -17,7 +16,7 @@ import (
"github.com/TheTNB/panel/pkg/db"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
)
@@ -54,7 +53,8 @@ func (r *backupRepo) List(typ biz.BackupType) ([]*types.BackupFile, error) {
list = append(list, &types.BackupFile{
Name: file.Name(),
Path: filepath.Join(path, file.Name()),
Size: str.FormatBytes(float64(info.Size())),
Size: tools.FormatBytes(float64(info.Size())),
Time: info.ModTime(),
})
}
@@ -134,7 +134,7 @@ func (r *backupRepo) CutoffLog(path, target string) error {
}
to := filepath.Join(path, fmt.Sprintf("%s_%s.zip", time.Now().Format("20060102150405"), filepath.Base(target)))
if err := io.Compress([]string{target}, to, io.Zip); err != nil {
if err := io.Compress(filepath.Dir(target), []string{filepath.Base(target)}, to); err != nil {
return err
}
@@ -180,9 +180,15 @@ func (r *backupRepo) ClearExpired(path, prefix string, save int) error {
toDelete := filtered[save:]
for _, file := range toDelete {
filePath := filepath.Join(path, file.Name())
color.Greenln(fmt.Sprintf("|-清理过期文件:%s", filePath))
if app.IsCli {
fmt.Printf("|-清理过期文件:%s\n", filePath)
}
if err = os.Remove(filePath); err != nil {
color.Redln(fmt.Sprintf("|-清理失败:%v", err))
if app.IsCli {
fmt.Printf("|-清理失败:%v\n", err)
} else {
return fmt.Errorf("清理失败:%v", err)
}
}
}
@@ -220,23 +226,16 @@ func (r *backupRepo) createWebsite(to string, name string) error {
return err
}
var paths []string
dirs, err := io.ReadDir(website.Path)
if err != nil {
return err
}
for _, item := range dirs {
paths = append(paths, filepath.Join(website.Path, item.Name()))
}
start := time.Now()
backup := filepath.Join(to, fmt.Sprintf("%s_%s.zip", website.Name, time.Now().Format("20060102150405")))
if err = io.Compress(paths, backup, io.Zip); err != nil {
if err = io.Compress(website.Path, nil, backup); err != nil {
return err
}
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-备份至文件%s", backup))
if app.IsCli {
fmt.Printf("|-备份耗时%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup))
}
return nil
}
@@ -246,7 +245,7 @@ func (r *backupRepo) createMySQL(to string, name string) error {
if err != nil {
return err
}
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
if err != nil {
return err
}
@@ -273,15 +272,17 @@ func (r *backupRepo) createMySQL(to string, name string) error {
return err
}
if err = io.Compress([]string{backup}, backup+".zip", io.Zip); err != nil {
if err = io.Compress(filepath.Dir(backup), []string{filepath.Base(backup)}, backup+".zip"); err != nil {
return err
}
if err = io.Remove(backup); err != nil {
return err
}
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-备份至文件%s", backup+".zip"))
if app.IsCli {
fmt.Printf("|-备份耗时%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup+".zip"))
}
return nil
}
@@ -308,15 +309,17 @@ func (r *backupRepo) createPostgres(to string, name string) error {
return err
}
if err = io.Compress([]string{backup}, backup+".zip", io.Zip); err != nil {
if err = io.Compress(filepath.Dir(backup), []string{filepath.Base(backup)}, backup+".zip"); err != nil {
return err
}
if err = io.Remove(backup); err != nil {
return err
}
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-备份至文件%s", backup+".zip"))
if app.IsCli {
fmt.Printf("|-备份耗时%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup+".zip"))
}
return nil
}
@@ -329,17 +332,36 @@ func (r *backupRepo) createPanel(to string) error {
}
start := time.Now()
if err := io.Compress([]string{
filepath.Join(app.Root, "panel"),
"/usr/local/sbin/panel-cli",
"/usr/local/etc/panel/config.yml",
}, backup, io.Zip); err != nil {
temp, err := io.TempDir("panel-backup")
if err != nil {
return err
}
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup))
return nil
if err = io.Cp(filepath.Join(app.Root, "panel"), temp); err != nil {
return err
}
if err = io.Cp("/usr/local/sbin/panel-cli", temp); err != nil {
return err
}
if err = io.Cp("/usr/local/etc/panel/config.yml", temp); err != nil {
return err
}
_ = io.Chmod(temp, 0600)
if err = io.Compress(temp, nil, backup); err != nil {
return err
}
if err = io.Chmod(backup, 0600); err != nil {
return err
}
if app.IsCli {
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup))
}
return io.Remove(temp)
}
// restoreWebsite 恢复网站备份
@@ -352,15 +374,11 @@ func (r *backupRepo) restoreWebsite(backup, target string) error {
if err != nil {
return err
}
format, err := io.FormatArchiveByPath(backup)
if err != nil {
return err
}
if err = io.Remove(website.Path); err != nil {
return err
}
if err = io.UnCompress(backup, website.Path, format); err != nil {
if err = io.UnCompress(backup, website.Path); err != nil {
return err
}
if err = io.Chmod(website.Path, 0755); err != nil {
@@ -383,7 +401,7 @@ func (r *backupRepo) restoreMySQL(backup, target string) error {
if err != nil {
return err
}
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
if err != nil {
return err
}
@@ -459,10 +477,12 @@ func (r *backupRepo) preCheckPath(to, path string) error {
return err
}
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
color.Greenln(fmt.Sprintf("|-目标文件数:%d", files))
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
color.Greenln(fmt.Sprintf("|-备份目录可用Inode%d", usage.InodesFree))
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)
}
if uint64(size) > usage.Free {
return errors.New("备份目录空间不足")
@@ -483,9 +503,11 @@ func (r *backupRepo) preCheckDB(to string, size int64) error {
return err
}
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
color.Greenln(fmt.Sprintf("|-备份目录可用空间%s", str.FormatBytes(float64(usage.Free))))
color.Greenln(fmt.Sprintf("|-备份目录可用Inode%d", usage.InodesFree))
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)
}
if uint64(size) > usage.Free {
return errors.New("备份目录空间不足")
@@ -501,11 +523,7 @@ func (r *backupRepo) autoUnCompressSQL(backup string) (string, error) {
return "", err
}
format, err := io.FormatArchiveByPath(backup)
if err != nil {
return "", err
}
if err = io.UnCompress(backup, temp, format); err != nil {
if err = io.UnCompress(backup, temp); err != nil {
return "", err
}
+113 -23
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"slices"
"strings"
"time"
@@ -11,27 +12,52 @@ import (
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/acme"
pkgcert "github.com/TheTNB/panel/pkg/cert"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/types"
)
type certRepo struct {
client *acme.Client
websiteRepo biz.WebsiteRepo
client *acme.Client
}
func NewCertRepo() biz.CertRepo {
return &certRepo{
websiteRepo: NewWebsiteRepo(),
}
return &certRepo{}
}
func (r *certRepo) List(page, limit uint) ([]*biz.Cert, int64, error) {
func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) {
var certs []*biz.Cert
var total int64
err := app.Orm.Model(&biz.Cert{}).Preload("Website").Preload("Account").Preload("DNS").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certs).Error
return certs, total, err
var list []*types.CertList
for cert := range slices.Values(certs) {
item := &types.CertList{
ID: cert.ID,
AccountID: cert.AccountID,
WebsiteID: cert.WebsiteID,
DNSID: cert.DNSID,
Type: cert.Type,
Domains: cert.Domains,
AutoRenew: cert.AutoRenew,
Cert: cert.Cert,
Key: cert.Key,
CreatedAt: cert.CreatedAt,
UpdatedAt: cert.UpdatedAt,
}
if decode, err := pkgcert.ParseCert(cert.Cert); err == nil {
item.NotBefore = decode.NotBefore
item.NotAfter = decode.NotAfter
item.Issuer = decode.Issuer.CommonName
item.OCSPServer = decode.OCSPServer
item.DNSNames = decode.DNSNames
}
list = append(list, item)
}
return list, total, err
}
func (r *certRepo) Get(id uint) (*biz.Cert, error) {
@@ -40,6 +66,34 @@ func (r *certRepo) Get(id uint) (*biz.Cert, error) {
return cert, err
}
func (r *certRepo) GetByWebsite(WebsiteID uint) (*biz.Cert, error) {
cert := new(biz.Cert)
err := app.Orm.Model(&biz.Cert{}).Preload("Website").Preload("Account").Preload("DNS").Where("website_id = ?", WebsiteID).First(cert).Error
return cert, err
}
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)
}
if _, err = pkgcert.ParseKey(req.Key); err != nil {
return nil, fmt.Errorf("failed to parse private key: %v", err)
}
cert := &biz.Cert{
Type: "upload",
Domains: info.DNSNames,
Cert: req.Cert,
Key: req.Key,
}
if err = app.Orm.Create(cert).Error; err != nil {
return nil, err
}
return cert, nil
}
func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) {
cert := &biz.Cert{
AccountID: req.AccountID,
@@ -56,11 +110,19 @@ func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) {
}
func (r *certRepo) Update(req *request.CertUpdate) error {
return app.Orm.Model(&biz.Cert{}).Where("id = ?", req.ID).Updates(&biz.Cert{
info, err := pkgcert.ParseCert(req.Cert)
if err == nil && req.Type == "upload" {
req.Domains = info.DNSNames
}
return app.Orm.Model(&biz.Cert{}).Where("id = ?", req.ID).Select("*").Updates(&biz.Cert{
ID: req.ID,
AccountID: req.AccountID,
WebsiteID: req.WebsiteID,
DNSID: req.DNSID,
Type: req.Type,
Cert: req.Cert,
Key: req.Key,
Domains: req.Domains,
AutoRenew: req.AutoRenew,
}).Error
@@ -85,19 +147,19 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data)
} else {
if cert.Website == nil {
return nil, errors.New("该证书没有关联网站,无法自动签发")
return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually")
} else {
for _, domain := range cert.Domains {
if strings.Contains(domain, "*") {
return nil, errors.New("通配符域名无法使用 HTTP 验证")
return nil, errors.New("wildcard domains cannot use HTTP verification")
}
}
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
client.UseHTTP(conf, cert.Website.Path)
client.UseHTTP(conf)
}
}
ssl, err := client.ObtainSSL(context.Background(), cert.Domains, acme.KeyType(cert.Type))
ssl, err := client.ObtainCertificate(context.Background(), cert.Domains, acme.KeyType(cert.Type))
if err != nil {
return nil, err
}
@@ -123,10 +185,10 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
}
if r.client == nil {
return nil, errors.New("请重新获取 DNS 解析记录")
return nil, errors.New("please retry the manual obtain operation")
}
ssl, err := r.client.ObtainSSLManual()
ssl, err := r.client.ObtainCertificateManual()
if err != nil {
return nil, err
}
@@ -145,6 +207,30 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
return &ssl, nil
}
func (r *certRepo) ObtainSelfSigned(id uint) error {
cert, err := r.Get(id)
if err != nil {
return err
}
crt, key, err := pkgcert.GenerateSelfSigned(cert.Domains)
if err != nil {
return err
}
cert.Cert = string(crt)
cert.Key = string(key)
if err = app.Orm.Save(cert).Error; err != nil {
return err
}
if cert.Website != nil {
return r.Deploy(cert.ID, cert.WebsiteID)
}
return nil
}
func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
cert, err := r.Get(id)
if err != nil {
@@ -157,26 +243,26 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
}
if cert.CertURL == "" {
return nil, errors.New("该证书没有签发成功,无法续签")
return nil, errors.New("this certificate has not been signed successfully and cannot be renewed")
}
if cert.DNS != nil {
client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data)
} else {
if cert.Website == nil {
return nil, errors.New("该证书没有关联网站,无法续签,可以尝试手动签发")
return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually")
} else {
for _, domain := range cert.Domains {
if strings.Contains(domain, "*") {
return nil, errors.New("通配符域名无法使用 HTTP 验证")
return nil, errors.New("wildcard domains cannot use HTTP verification")
}
}
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
client.UseHTTP(conf, cert.Website.Path)
client.UseHTTP(conf)
}
}
ssl, err := client.RenewSSL(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
ssl, err := client.RenewCertificate(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
if err != nil {
return nil, err
}
@@ -228,18 +314,18 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
}
if cert.Cert == "" || cert.Key == "" {
return errors.New("该证书没有签发成功,无法部署")
return errors.New("this certificate has not been signed successfully and cannot be deployed")
}
website, err := r.websiteRepo.Get(WebsiteID)
website, err := NewWebsiteRepo().Get(WebsiteID)
if err != nil {
return err
}
if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.pem", app.Root, website.Name), cert.Cert, 0644); err != nil {
if err = io.Write(fmt.Sprintf("%s/server/vhost/cert/%s.pem", app.Root, website.Name), cert.Cert, 0644); err != nil {
return err
}
if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.key", app.Root, website.Name), cert.Key, 0644); err != nil {
if err = io.Write(fmt.Sprintf("%s/server/vhost/cert/%s.key", app.Root, website.Name), cert.Key, 0644); err != nil {
return err
}
if err = systemctl.Reload("nginx"); err != nil {
@@ -251,6 +337,10 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
}
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")
}
var ca string
var eab *acme.EAB
switch cert.Account.CA {
+31 -11
View File
@@ -28,6 +28,26 @@ func (r certAccountRepo) List(page, limit uint) ([]*biz.CertAccount, int64, erro
return accounts, total, err
}
func (r certAccountRepo) GetDefault(userID uint) (*biz.CertAccount, error) {
user, err := NewUserRepo().Get(userID)
if err != nil {
return nil, err
}
account := new(biz.CertAccount)
if err = app.Orm.Model(&biz.CertAccount{}).Where("ca = ?", "googlecn").Where("email = ?", user.Email).First(account).Error; err == nil {
return account, nil
}
req := &request.CertAccountCreate{
CA: "googlecn",
Email: user.Email,
KeyType: string(acme.KeyEC256),
}
return r.Create(req)
}
func (r certAccountRepo) Get(id uint) (*biz.CertAccount, error) {
account := new(biz.CertAccount)
err := app.Orm.Model(&biz.CertAccount{}).Where("id = ?", id).First(account).Error
@@ -70,16 +90,16 @@ func (r certAccountRepo) Create(req *request.CertAccountCreate) (*biz.CertAccoun
case "sslcom":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
default:
return nil, errors.New("CA 提供商不支持")
return nil, errors.New("unsupported CA")
}
if err != nil {
return nil, fmt.Errorf("注册账号失败:%v", err)
return nil, fmt.Errorf("failed to register account: %v", err)
}
privateKey, err := cert.EncodeKey(client.Account.PrivateKey)
if err != nil {
return nil, errors.New("获取私钥失败")
return nil, errors.New("failed to get private key")
}
account.PrivateKey = string(privateKey)
@@ -129,16 +149,16 @@ func (r certAccountRepo) Update(req *request.CertAccountUpdate) error {
case "sslcom":
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
default:
return errors.New("CA 提供商不支持")
return errors.New("unsupported CA")
}
if err != nil {
return errors.New("向 CA 注册账号失败,请检查参数是否正确")
return errors.New("failed to register account")
}
privateKey, err := cert.EncodeKey(client.Account.PrivateKey)
if err != nil {
return errors.New("获取私钥失败")
return errors.New("failed to get private key")
}
account.PrivateKey = string(privateKey)
@@ -162,13 +182,13 @@ func (r certAccountRepo) getGoogleEAB() (*acme.EAB, error) {
client.SetTimeout(5 * time.Second)
client.SetRetryCount(2)
resp, err := client.R().SetResult(&data{}).Get("https://panel.haozi.net/api/acme/googleEAB")
resp, err := client.R().SetResult(&data{}).Get("https://gts.rat.dev/eab")
if err != nil || !resp.IsSuccess() {
return &acme.EAB{}, errors.New("获取Google EAB失败")
return &acme.EAB{}, fmt.Errorf("failed to get Google EAB: %v", err)
}
eab := resp.Result().(*data)
if eab.Message != "success" {
return &acme.EAB{}, errors.New("获取Google EAB失败")
return &acme.EAB{}, fmt.Errorf("failed to get Google EAB: %s", eab.Message)
}
return &acme.EAB{KeyID: eab.Data.KeyId, MACKey: eab.Data.MacKey}, nil
@@ -189,11 +209,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{}, errors.New("获取ZeroSSL EAB失败")
return &acme.EAB{}, fmt.Errorf("failed to get ZeroSSL EAB: %v", err)
}
eab := resp.Result().(*data)
if !eab.Success {
return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败")
return &acme.EAB{}, fmt.Errorf("failed to get ZeroSSL EAB")
}
return &acme.EAB{KeyID: eab.EabKid, MACKey: eab.EabHmacKey}, nil
+141 -150
View File
@@ -3,233 +3,224 @@ package data
import (
"context"
"fmt"
"io"
"strconv"
"net"
"net/http"
"slices"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
"github.com/go-resty/resty/v2"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
paneltypes "github.com/TheTNB/panel/pkg/types"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/types"
"github.com/TheTNB/panel/pkg/types/docker/container"
)
type containerRepo struct {
client *client.Client
client *resty.Client
}
func NewContainerRepo(sock ...string) biz.ContainerRepo {
if len(sock) == 0 {
sock = append(sock, "/run/podman/podman.sock")
sock = append(sock, "/var/run/docker.sock")
}
cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation())
client := resty.New()
client.SetTimeout(1 * time.Minute)
client.SetRetryCount(2)
client.SetTransport(&http.Transport{
DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", sock[0])
},
})
client.SetBaseURL("http://d/v1.40")
return &containerRepo{
client: cli,
client: client,
}
}
// ListAll 列出所有容器
func (r *containerRepo) ListAll() ([]types.Container, error) {
containers, err := r.client.ContainerList(context.Background(), container.ListOptions{
All: true,
})
var resp []container.Container
_, err := r.client.R().SetResult(&resp).SetQueryParam("all", "true").Get("/containers/json")
if err != nil {
return nil, err
}
var containers []types.Container
for _, item := range resp {
ports := make([]types.ContainerPort, 0)
for _, port := range item.Ports {
ports = append(ports, types.ContainerPort{
ContainerStart: uint(port.PrivatePort),
ContainerEnd: uint(port.PublicPort),
HostStart: uint(port.PublicPort),
HostEnd: uint(port.PublicPort),
Protocol: port.Type,
Host: port.IP,
})
}
if len(item.Names) == 0 {
item.Names = append(item.Names, "")
}
containers = append(containers, types.Container{
ID: item.ID,
Name: strings.TrimPrefix(item.Names[0], "/"), // https://github.com/moby/moby/issues/7519
Image: item.Image,
ImageID: item.ImageID,
Command: item.Command,
CreatedAt: time.Unix(item.Created, 0),
State: item.State,
Status: item.Status,
Ports: ports,
Labels: types.MapToKV(item.Labels),
})
}
slices.SortFunc(containers, func(a types.Container, b types.Container) int {
return strings.Compare(a.Name, b.Name)
})
return containers, nil
}
// ListByNames 根据名称列出容器
func (r *containerRepo) ListByNames(names []string) ([]types.Container, error) {
var options container.ListOptions
options.All = true
if len(names) > 0 {
var array []filters.KeyValuePair
for _, n := range names {
array = append(array, filters.Arg("name", n))
}
options.Filters = filters.NewArgs(array...)
}
containers, err := r.client.ContainerList(context.Background(), options)
// ListByName 根据名称搜索容器
func (r *containerRepo) ListByName(names string) ([]types.Container, error) {
containers, err := r.ListAll()
if err != nil {
return nil, err
}
containers = slices.DeleteFunc(containers, func(item types.Container) bool {
return !strings.Contains(item.Name, names)
})
return containers, nil
}
// Create 创建容器
func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) {
var hostConf container.HostConfig
var networkConf network.NetworkingConfig
portMap := make(nat.PortMap)
for _, port := range req.Ports {
if port.ContainerStart-port.ContainerEnd != port.HostStart-port.HostEnd {
return "", fmt.Errorf("容器端口和主机端口数量不匹配(容器: %d 主机: %d)", port.ContainerStart-port.ContainerEnd, port.HostStart-port.HostEnd)
}
if port.ContainerStart > port.ContainerEnd || port.HostStart > port.HostEnd || port.ContainerStart < 1 || port.HostStart < 1 {
return "", fmt.Errorf("端口范围不正确")
}
count := 0
for host := port.HostStart; host <= port.HostEnd; host++ {
bindItem := nat.PortBinding{HostPort: strconv.Itoa(host), HostIP: port.Host}
portMap[nat.Port(fmt.Sprintf("%d/%s", port.ContainerStart+count, port.Protocol))] = []nat.PortBinding{bindItem}
count++
}
}
exposed := make(nat.PortSet)
for port := range portMap {
exposed[port] = struct{}{}
}
if req.Network != "" {
switch req.Network {
case "host", "none", "bridge":
hostConf.NetworkMode = container.NetworkMode(req.Network)
}
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{req.Network: {}}
var sb strings.Builder
sb.WriteString(fmt.Sprintf("docker run -d --name %s", req.Name))
if req.PublishAllPorts {
sb.WriteString(" -P")
} else {
networkConf = network.NetworkingConfig{}
for _, port := range req.Ports {
sb.WriteString(" -p ")
if port.Host != "" {
sb.WriteString(fmt.Sprintf("%s:", port.Host))
}
if port.HostStart == port.HostEnd || port.ContainerStart == port.ContainerEnd {
sb.WriteString(fmt.Sprintf("%d:%d/%s", port.HostStart, port.ContainerStart, port.Protocol))
} else {
sb.WriteString(fmt.Sprintf("%d-%d:%d-%d/%s", port.HostStart, port.HostEnd, port.ContainerStart, port.ContainerEnd, port.Protocol))
}
}
}
if req.Network != "" {
sb.WriteString(fmt.Sprintf(" --network %s", req.Network))
}
for _, volume := range req.Volumes {
sb.WriteString(fmt.Sprintf(" -v %s:%s:%s", volume.Host, volume.Container, volume.Mode))
}
for _, label := range req.Labels {
sb.WriteString(fmt.Sprintf(" --label %s=%s", label.Key, label.Value))
}
for _, env := range req.Env {
sb.WriteString(fmt.Sprintf(" -e %s=%s", env.Key, env.Value))
}
if len(req.Entrypoint) > 0 {
sb.WriteString(fmt.Sprintf(" --entrypoint '%s'", strings.Join(req.Entrypoint, " ")))
}
if len(req.Command) > 0 {
sb.WriteString(fmt.Sprintf(" '%s'", strings.Join(req.Command, " ")))
}
if req.RestartPolicy != "" {
sb.WriteString(fmt.Sprintf(" --restart %s", req.RestartPolicy))
}
if req.AutoRemove {
sb.WriteString(" --rm")
}
if req.Privileged {
sb.WriteString(" --privileged")
}
if req.OpenStdin {
sb.WriteString(" -i")
}
if req.Tty {
sb.WriteString(" -t")
}
if req.CPUShares > 0 {
sb.WriteString(fmt.Sprintf(" --cpu-shares %d", req.CPUShares))
}
if req.CPUs > 0 {
sb.WriteString(fmt.Sprintf(" --cpus %d", req.CPUs))
}
if req.Memory > 0 {
sb.WriteString(fmt.Sprintf(" --memory %d", req.Memory))
}
hostConf.Privileged = req.Privileged
hostConf.AutoRemove = req.AutoRemove
hostConf.CPUShares = req.CPUShares
hostConf.PublishAllPorts = req.PublishAllPorts
hostConf.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(req.RestartPolicy)}
if req.RestartPolicy == "on-failure" {
hostConf.RestartPolicy.MaximumRetryCount = 5
}
hostConf.NanoCPUs = req.CPUs * 1000000000
hostConf.Memory = req.Memory * 1024 * 1024
hostConf.MemorySwap = 0
hostConf.PortBindings = portMap
hostConf.Binds = []string{}
volumes := make(map[string]struct{})
for _, v := range req.Volumes {
volumes[v.Container] = struct{}{}
hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", v.Host, v.Container, v.Mode))
}
resp, err := r.client.ContainerCreate(context.Background(), &container.Config{
Image: req.Image,
Env: paneltypes.KVToSlice(req.Env),
Entrypoint: req.Entrypoint,
Cmd: req.Command,
Labels: paneltypes.KVToMap(req.Labels),
ExposedPorts: exposed,
OpenStdin: req.OpenStdin,
Tty: req.Tty,
Volumes: volumes,
}, &hostConf, &networkConf, nil, req.Name)
if err != nil {
return "", err
}
return resp.ID, err
sb.WriteString(" %s bash")
return shell.ExecfWithTTY(sb.String(), req.Image)
}
// Remove 移除容器
func (r *containerRepo) Remove(id string) error {
return r.client.ContainerRemove(context.Background(), id, container.RemoveOptions{
Force: true,
})
_, err := shell.ExecfWithTimeout(120*time.Second, "docker rm -f %s", id)
return err
}
// Start 启动容器
func (r *containerRepo) Start(id string) error {
return r.client.ContainerStart(context.Background(), id, container.StartOptions{})
_, err := shell.ExecfWithTimeout(120*time.Second, "docker start %s", id)
return err
}
// Stop 停止容器
func (r *containerRepo) Stop(id string) error {
return r.client.ContainerStop(context.Background(), id, container.StopOptions{})
_, err := shell.ExecfWithTimeout(120*time.Second, "docker stop %s", id)
return err
}
// Restart 重启容器
func (r *containerRepo) Restart(id string) error {
return r.client.ContainerRestart(context.Background(), id, container.StopOptions{})
_, err := shell.ExecfWithTimeout(120*time.Second, "docker restart %s", id)
return err
}
// Pause 暂停容器
func (r *containerRepo) Pause(id string) error {
return r.client.ContainerPause(context.Background(), id)
_, err := shell.ExecfWithTimeout(120*time.Second, "docker pause %s", id)
return err
}
// Unpause 恢复容器
func (r *containerRepo) Unpause(id string) error {
return r.client.ContainerUnpause(context.Background(), id)
}
// Inspect 查看容器
func (r *containerRepo) Inspect(id string) (types.ContainerJSON, error) {
return r.client.ContainerInspect(context.Background(), id)
_, err := shell.ExecfWithTimeout(120*time.Second, "docker unpause %s", id)
return err
}
// Kill 杀死容器
func (r *containerRepo) Kill(id string) error {
return r.client.ContainerKill(context.Background(), id, "KILL")
_, err := shell.ExecfWithTimeout(120*time.Second, "docker kill %s", id)
return err
}
// Rename 重命名容器
func (r *containerRepo) Rename(id string, newName string) error {
return r.client.ContainerRename(context.Background(), id, newName)
}
// Stats 查看容器状态
func (r *containerRepo) Stats(id string) (container.StatsResponseReader, error) {
return r.client.ContainerStats(context.Background(), id, false)
}
// Exist 判断容器是否存在
func (r *containerRepo) Exist(name string) (bool, error) {
var options container.ListOptions
options.Filters = filters.NewArgs(filters.Arg("name", name))
containers, err := r.client.ContainerList(context.Background(), options)
if err != nil {
return false, err
}
return len(containers) > 0, nil
}
// Update 更新容器
func (r *containerRepo) Update(id string, config container.UpdateConfig) error {
_, err := r.client.ContainerUpdate(context.Background(), id, config)
_, err := shell.ExecfWithTimeout(120*time.Second, "docker rename %s %s", id, newName)
return err
}
// Logs 查看容器日志
func (r *containerRepo) Logs(id string) (string, error) {
options := container.LogsOptions{
ShowStdout: true,
ShowStderr: true,
}
reader, err := r.client.ContainerLogs(context.Background(), id, options)
if err != nil {
return "", err
}
defer reader.Close()
data, err := io.ReadAll(reader)
if err != nil {
return "", err
}
return string(data), nil
return shell.ExecfWithTimeout(120*time.Second, "docker logs %s", id)
}
// Prune 清理未使用的容器
func (r *containerRepo) Prune() error {
_, err := r.client.ContainersPrune(context.Background(), filters.NewArgs())
_, err := shell.ExecfWithTimeout(120*time.Second, "docker container prune -f")
return err
}
+58 -51
View File
@@ -2,96 +2,103 @@ package data
import (
"context"
"encoding/base64"
"encoding/json"
"io"
"fmt"
"net"
"net/http"
"slices"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/client"
"github.com/go-resty/resty/v2"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
"github.com/TheTNB/panel/pkg/types/docker/image"
)
type containerImageRepo struct {
client *client.Client
client *resty.Client
}
func NewContainerImageRepo(sock ...string) biz.ContainerImageRepo {
if len(sock) == 0 {
sock = append(sock, "/run/podman/podman.sock")
sock = append(sock, "/var/run/docker.sock")
}
cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation())
client := resty.New()
client.SetTimeout(1 * time.Minute)
client.SetRetryCount(2)
client.SetTransport(&http.Transport{
DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", sock[0])
},
})
client.SetBaseURL("http://d/v1.40")
return &containerImageRepo{
client: cli,
client: client,
}
}
// List 列出镜像
func (r *containerImageRepo) List() ([]image.Summary, error) {
return r.client.ImageList(context.Background(), image.ListOptions{
All: true,
})
}
// Exist 判断镜像是否存在
func (r *containerImageRepo) Exist(id string) (bool, error) {
var options image.ListOptions
options.Filters = filters.NewArgs(filters.Arg("reference", id))
images, err := r.client.ImageList(context.Background(), options)
func (r *containerImageRepo) List() ([]types.ContainerImage, error) {
var resp []image.Image
_, err := r.client.R().SetResult(&resp).SetQueryParam("all", "true").Get("/images/json")
if err != nil {
return false, err
return nil, err
}
return len(images) > 0, nil
var images []types.ContainerImage
for _, item := range resp {
images = append(images, types.ContainerImage{
ID: item.ID,
Containers: item.Containers,
RepoTags: item.RepoTags,
RepoDigests: item.RepoDigests,
Size: tools.FormatBytes(float64(item.Size)),
Labels: types.MapToKV(item.Labels),
CreatedAt: time.Unix(item.Created, 0),
})
}
slices.SortFunc(images, func(a types.ContainerImage, b types.ContainerImage) int {
return strings.Compare(a.ID, b.ID)
})
return images, nil
}
// Pull 拉取镜像
func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error {
options := image.PullOptions{}
var sb strings.Builder
if req.Auth {
authConfig := registry.AuthConfig{
Username: req.Username,
Password: req.Password,
sb.WriteString(fmt.Sprintf("docker login -u %s -p %s", req.Username, req.Password))
if _, err := shell.Exec(sb.String()); err != nil {
return fmt.Errorf("login failed: %w", err)
}
encodedJSON, err := json.Marshal(authConfig)
if err != nil {
return err
}
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
options.RegistryAuth = authStr
sb.Reset()
}
out, err := r.client.ImagePull(context.Background(), req.Name, options)
if err != nil {
sb.WriteString(fmt.Sprintf("docker pull %s", req.Name))
if _, err := shell.Exec(sb.String()); err != nil {
return err
}
defer out.Close()
_, err = io.Copy(io.Discard, out)
return err
return nil
}
// Remove 删除镜像
func (r *containerImageRepo) Remove(id string) error {
_, err := r.client.ImageRemove(context.Background(), id, image.RemoveOptions{
Force: true,
PruneChildren: true,
})
_, err := shell.ExecfWithTimeout(120*time.Second, "docker rmi %s", id)
return err
}
// Prune 清理未使用的镜像
func (r *containerImageRepo) Prune() error {
_, err := r.client.ImagesPrune(context.Background(), filters.NewArgs())
_, err := shell.ExecfWithTimeout(120*time.Second, "docker image prune -f")
return err
}
// Inspect 查看镜像
func (r *containerImageRepo) Inspect(id string) (types.ImageInspect, error) {
img, _, err := r.client.ImageInspectWithRaw(context.Background(), id)
return img, err
}
+91 -64
View File
@@ -2,103 +2,130 @@ package data
import (
"context"
"fmt"
"net"
"net/http"
"slices"
"strings"
"time"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/go-resty/resty/v2"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/types"
"github.com/TheTNB/panel/pkg/types/docker/network"
)
type containerNetworkRepo struct {
client *client.Client
client *resty.Client
}
func NewContainerNetworkRepo(sock ...string) biz.ContainerNetworkRepo {
if len(sock) == 0 {
sock = append(sock, "/run/podman/podman.sock")
sock = append(sock, "/var/run/docker.sock")
}
cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation())
client := resty.New()
client.SetTimeout(1 * time.Minute)
client.SetRetryCount(2)
client.SetTransport(&http.Transport{
DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", sock[0])
},
})
client.SetBaseURL("http://d/v1.40")
return &containerNetworkRepo{
client: cli,
client: client,
}
}
// List 列出网络
func (r *containerNetworkRepo) List() ([]network.Inspect, error) {
return r.client.NetworkList(context.Background(), network.ListOptions{})
func (r *containerNetworkRepo) List() ([]types.ContainerNetwork, error) {
var resp []network.Network
_, err := r.client.R().SetResult(&resp).Get("/networks")
if err != nil {
return nil, err
}
var networks []types.ContainerNetwork
for _, item := range resp {
ipamConfigs := make([]types.ContainerNetworkIPAMConfig, 0)
for _, ipam := range item.IPAM.Config {
ipamConfigs = append(ipamConfigs, types.ContainerNetworkIPAMConfig{
Subnet: ipam.Subnet,
IPRange: ipam.IPRange,
Gateway: ipam.Gateway,
AuxAddress: ipam.AuxAddress,
})
}
networks = append(networks, types.ContainerNetwork{
ID: item.ID,
Name: item.Name,
Driver: item.Driver,
IPv6: item.EnableIPv6,
Internal: item.Internal,
Attachable: item.Attachable,
Ingress: item.Ingress,
Scope: item.Scope,
CreatedAt: item.Created,
IPAM: types.ContainerNetworkIPAM{
Driver: item.IPAM.Driver,
Options: types.MapToKV(item.IPAM.Options),
Config: ipamConfigs,
},
Options: types.MapToKV(item.Options),
Labels: types.MapToKV(item.Labels),
})
}
slices.SortFunc(networks, func(a types.ContainerNetwork, b types.ContainerNetwork) int {
return strings.Compare(a.Name, b.Name)
})
return networks, nil
}
// Create 创建网络
func (r *containerNetworkRepo) Create(req *request.ContainerNetworkCreate) (string, error) {
var ipamConfigs []network.IPAMConfig
if req.Ipv4.Enabled {
ipamConfigs = append(ipamConfigs, network.IPAMConfig{
Subnet: req.Ipv4.Subnet,
Gateway: req.Ipv4.Gateway,
IPRange: req.Ipv4.IPRange,
})
}
if req.Ipv6.Enabled {
ipamConfigs = append(ipamConfigs, network.IPAMConfig{
Subnet: req.Ipv6.Subnet,
Gateway: req.Ipv6.Gateway,
IPRange: req.Ipv6.IPRange,
})
}
var sb strings.Builder
options := network.CreateOptions{
EnableIPv6: &req.Ipv6.Enabled,
Driver: req.Driver,
Options: types.KVToMap(req.Options),
Labels: types.KVToMap(req.Labels),
}
if len(ipamConfigs) > 0 {
options.IPAM = &network.IPAM{
Config: ipamConfigs,
sb.WriteString(fmt.Sprintf("docker network create --driver %s", req.Driver))
sb.WriteString(fmt.Sprintf(" %s", req.Name))
if req.Ipv4.Enabled {
sb.WriteString(fmt.Sprintf(" --subnet %s", req.Ipv4.Subnet))
sb.WriteString(fmt.Sprintf(" --gateway %s", req.Ipv4.Gateway))
if req.Ipv4.IPRange != "" {
sb.WriteString(fmt.Sprintf(" --ip-range %s", req.Ipv4.IPRange))
}
}
if req.Ipv6.Enabled {
sb.WriteString(fmt.Sprintf(" --subnet %s", req.Ipv6.Subnet))
sb.WriteString(fmt.Sprintf(" --gateway %s", req.Ipv6.Gateway))
if req.Ipv6.IPRange != "" {
sb.WriteString(fmt.Sprintf(" --ip-range %s", req.Ipv6.IPRange))
}
}
for _, label := range req.Labels {
sb.WriteString(fmt.Sprintf(" --label %s=%s", label.Key, label.Value))
}
for _, option := range req.Options {
sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value))
}
resp, err := r.client.NetworkCreate(context.Background(), req.Name, options)
return resp.ID, err
return shell.Exec(sb.String())
}
// Remove 删除网络
func (r *containerNetworkRepo) Remove(id string) error {
return r.client.NetworkRemove(context.Background(), id)
}
// Exist 判断网络是否存在
func (r *containerNetworkRepo) Exist(name string) (bool, error) {
var options network.ListOptions
options.Filters = filters.NewArgs(filters.Arg("name", name))
networks, err := r.client.NetworkList(context.Background(), options)
if err != nil {
return false, err
}
return len(networks) > 0, nil
}
// Inspect 查看网络
func (r *containerNetworkRepo) Inspect(id string) (network.Inspect, error) {
return r.client.NetworkInspect(context.Background(), id, network.InspectOptions{})
}
// Connect 连接网络
func (r *containerNetworkRepo) Connect(networkID string, containerID string) error {
return r.client.NetworkConnect(context.Background(), networkID, containerID, nil)
}
// Disconnect 断开网络
func (r *containerNetworkRepo) Disconnect(networkID string, containerID string) error {
return r.client.NetworkDisconnect(context.Background(), networkID, containerID, true)
_, err := shell.ExecfWithTimeout(120*time.Second, "docker network rm -f %s", id)
return err
}
// Prune 清理未使用的网络
func (r *containerNetworkRepo) Prune() error {
_, err := r.client.NetworksPrune(context.Background(), filters.NewArgs())
_, err := shell.ExecfWithTimeout(120*time.Second, "docker network prune -f")
return err
}
+63 -32
View File
@@ -2,73 +2,104 @@ package data
import (
"context"
"fmt"
"net"
"net/http"
"slices"
"strings"
"time"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/client"
"github.com/go-resty/resty/v2"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
"github.com/TheTNB/panel/pkg/types/docker/volume"
)
type containerVolumeRepo struct {
client *client.Client
client *resty.Client
}
func NewContainerVolumeRepo(sock ...string) biz.ContainerVolumeRepo {
if len(sock) == 0 {
sock = append(sock, "/run/podman/podman.sock")
sock = append(sock, "/var/run/docker.sock")
}
cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation())
client := resty.New()
client.SetTimeout(1 * time.Minute)
client.SetRetryCount(2)
client.SetTransport(&http.Transport{
DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", sock[0])
},
})
client.SetBaseURL("http://d/v1.40")
return &containerVolumeRepo{
client: cli,
client: client,
}
}
// List 列出存储卷
func (r *containerVolumeRepo) List() ([]*volume.Volume, error) {
volumes, err := r.client.VolumeList(context.Background(), volume.ListOptions{})
func (r *containerVolumeRepo) List() ([]types.ContainerVolume, error) {
var resp volume.ListResponse
_, err := r.client.R().SetResult(&resp).Get("/volumes")
if err != nil {
return nil, err
}
return volumes.Volumes, err
var volumes []types.ContainerVolume
for _, item := range resp.Volumes {
volumes = append(volumes, types.ContainerVolume{
Name: item.Name,
Driver: item.Driver,
Scope: item.Scope,
MountPoint: item.Mountpoint,
CreatedAt: item.CreatedAt,
Labels: types.MapToKV(item.Labels),
Options: types.MapToKV(item.Options),
RefCount: item.UsageData.RefCount,
Size: tools.FormatBytes(float64(item.UsageData.Size)),
})
}
slices.SortFunc(volumes, func(a types.ContainerVolume, b types.ContainerVolume) int {
return strings.Compare(a.Name, b.Name)
})
return volumes, nil
}
// Create 创建存储卷
func (r *containerVolumeRepo) Create(req *request.ContainerVolumeCreate) (volume.Volume, error) {
return r.client.VolumeCreate(context.Background(), volume.CreateOptions{
Name: req.Name,
Driver: req.Driver,
DriverOpts: types.KVToMap(req.Options),
Labels: types.KVToMap(req.Labels),
})
}
func (r *containerVolumeRepo) Create(req *request.ContainerVolumeCreate) (string, error) {
var sb strings.Builder
sb.WriteString("docker volume create")
sb.WriteString(fmt.Sprintf(" %s", req.Name))
// Exist 判断存储卷是否存在
func (r *containerVolumeRepo) Exist(id string) (bool, error) {
var options volume.ListOptions
options.Filters = filters.NewArgs(filters.Arg("name", id))
volumes, err := r.client.VolumeList(context.Background(), options)
if err != nil {
return false, err
if req.Driver != "" {
sb.WriteString(fmt.Sprintf(" --driver %s", req.Driver))
}
for _, label := range req.Labels {
sb.WriteString(fmt.Sprintf(" --label %s=%s", label.Key, label.Value))
}
return len(volumes.Volumes) > 0, nil
}
for _, option := range req.Options {
sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value))
}
// Inspect 查看存储卷
func (r *containerVolumeRepo) Inspect(id string) (volume.Volume, error) {
return r.client.VolumeInspect(context.Background(), id)
return shell.Exec(sb.String())
}
// Remove 删除存储卷
func (r *containerVolumeRepo) Remove(id string) error {
return r.client.VolumeRemove(context.Background(), id, true)
_, err := shell.ExecfWithTimeout(120*time.Second, "docker volume rm -f %s", id)
return err
}
// Prune 清理未使用的存储卷
func (r *containerVolumeRepo) Prune() error {
_, err := r.client.VolumesPrune(context.Background(), filters.NewArgs())
_, err := shell.ExecfWithTimeout(120*time.Second, "docker volume prune -f")
return err
}
+10 -33
View File
@@ -4,17 +4,17 @@ import (
"errors"
"fmt"
"path/filepath"
"regexp"
"strconv"
"time"
"github.com/go-rat/utils/str"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/os"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
"github.com/TheTNB/panel/pkg/systemctl"
)
@@ -54,10 +54,6 @@ func (r *cronRepo) Get(id uint) (*biz.Cron, error) {
}
func (r *cronRepo) Create(req *request.CronCreate) error {
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) {
return errors.New("时间格式错误")
}
var script string
if req.Type == "backup" {
if req.BackupType == "website" {
@@ -66,8 +62,8 @@ 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
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)
}
if req.BackupType == "mysql" || req.BackupType == "postgres" {
@@ -76,8 +72,8 @@ 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
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)
}
}
@@ -92,6 +88,9 @@ 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)
}
if req.Type == "shell" {
script = req.Script
}
shellDir := fmt.Sprintf("%s/server/cron/", app.Root)
shellLogDir := fmt.Sprintf("%s/server/cron/logs/", app.Root)
@@ -101,7 +100,7 @@ panel-cli cutoff clear -t website -f %s -s %d -p %s
if !io.Exists(shellLogDir) {
return errors.New("计划任务日志目录不存在")
}
shellFile := strconv.Itoa(int(time.Now().Unix())) + str.RandomString(16)
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())
}
@@ -133,10 +132,6 @@ func (r *cronRepo) Update(req *request.CronUpdate) error {
return err
}
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) {
return errors.New("时间格式错误")
}
if !cron.Status {
return errors.New("计划任务已禁用")
}
@@ -200,24 +195,6 @@ func (r *cronRepo) Status(id uint, status bool) error {
return app.Orm.Save(cron).Error
}
func (r *cronRepo) Log(id uint) (string, error) {
cron, err := r.Get(id)
if err != nil {
return "", err
}
if !io.Exists(cron.Log) {
return "", errors.New("日志文件不存在")
}
log, err := shell.Execf("tail -n 1000 '%s'", cron.Log)
if err != nil {
return "", err
}
return log, nil
}
// addToSystem 添加到系统
func (r *cronRepo) addToSystem(cron *biz.Cron) error {
if _, err := shell.Execf(`( crontab -l; echo "%s %s >> %s 2>&1" ) | sort - | uniq - | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil {
+66
View File
@@ -0,0 +1,66 @@
package data
import (
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
)
type databaseRepo struct{}
func NewDatabaseRepo() biz.DatabaseRepo {
return &databaseRepo{}
}
func (d databaseRepo) Count() (int64, error) {
var count int64
if err := app.Orm.Model(&biz.Database{}).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (d databaseRepo) List(page, limit uint) ([]*biz.Database, int64, error) {
var database []*biz.Database
var total int64
err := app.Orm.Model(&biz.Database{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&database).Error
return database, total, err
}
func (d databaseRepo) Get(id uint) (*biz.Database, error) {
database := new(biz.Database)
if err := app.Orm.Where("id = ?", id).First(database).Error; err != nil {
return nil, err
}
return database, nil
}
func (d databaseRepo) Create(req *request.DatabaseCreate) error {
database := &biz.Database{
Name: req.Name,
Username: req.Username,
Password: req.Password,
ServerID: req.ServerID,
Status: biz.DatabaseStatusInvalid,
Remark: req.Remark,
}
return app.Orm.Create(database).Error
}
func (d databaseRepo) Update(req *request.DatabaseUpdate) error {
database := &biz.Database{
Name: req.Name,
Username: req.Username,
Password: req.Password,
Remark: req.Remark,
}
return app.Orm.Model(database).Where("id = ?", req.ID).Updates(database).Error
}
func (d databaseRepo) Delete(id uint) error {
return app.Orm.Delete(&biz.Database{}, id).Error
}
+83
View File
@@ -0,0 +1,83 @@
package data
import (
"errors"
"slices"
"strings"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
)
type databaseServerRepo struct{}
func NewDatabaseServerRepo() biz.DatabaseServerRepo {
return &databaseServerRepo{}
}
func (d databaseServerRepo) Count() (int64, error) {
var count int64
if err := app.Orm.Model(&biz.DatabaseServer{}).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (d databaseServerRepo) List(page, limit uint) ([]*biz.DatabaseServer, int64, error) {
var databaseServer []*biz.DatabaseServer
var total int64
err := app.Orm.Model(&biz.DatabaseServer{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&databaseServer).Error
return databaseServer, total, err
}
func (d databaseServerRepo) Get(id uint) (*biz.DatabaseServer, error) {
databaseServer := new(biz.DatabaseServer)
if err := app.Orm.Where("id = ?", id).First(databaseServer).Error; err != nil {
return nil, err
}
return databaseServer, nil
}
func (d databaseServerRepo) Create(req *request.DatabaseServerCreate) error {
databaseServer := &biz.DatabaseServer{
Name: req.Name,
Type: biz.DatabaseType(req.Type),
Host: req.Host,
Port: req.Port,
Username: req.Username,
Password: req.Password,
Remark: req.Remark,
}
return app.Orm.Create(databaseServer).Error
}
func (d databaseServerRepo) Update(req *request.DatabaseServerCreate) error {
databaseServer := &biz.DatabaseServer{
Name: req.Name,
Type: biz.DatabaseType(req.Type),
Host: req.Host,
Port: req.Port,
Username: req.Username,
Password: req.Password,
Remark: req.Remark,
}
return app.Orm.Save(databaseServer).Error
}
func (d databaseServerRepo) Delete(id uint) error {
ds := new(biz.DatabaseServer)
if err := app.Orm.Where("id = ?", id).First(ds).Error; err != nil {
return err
}
if slices.Contains([]string{"local_mysql", "local_postgresql", "local_redis"}, ds.Name) && !app.IsCli {
return errors.New("can't delete " + ds.Name + ", if you must delete it, please uninstall " + strings.TrimPrefix(ds.Name, "local_"))
}
return app.Orm.Delete(&biz.DatabaseServer{}, id).Error
}
+1 -1
View File
@@ -50,7 +50,7 @@ func (r monitorRepo) UpdateSetting(setting *request.MonitorSetting) error {
}
func (r monitorRepo) Clear() error {
return app.Orm.Delete(&biz.Monitor{}).Error
return app.Orm.Where("1 = 1").Delete(&biz.Monitor{}).Error
}
func (r monitorRepo) List(start, end time.Time) ([]*biz.Monitor, error) {
+13 -5
View File
@@ -1,12 +1,13 @@
package data
import (
"errors"
"fmt"
"strings"
"github.com/spf13/cast"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/pkg/firewall"
"github.com/TheTNB/panel/pkg/os"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
@@ -60,9 +61,9 @@ func (r *safeRepo) UpdateSSH(port uint, status bool) error {
}
func (r *safeRepo) GetPingStatus() (bool, error) {
out, err := shell.Execf(`firewall-cmd --list-all`)
if err != nil {
return true, errors.New(out)
out, err := shell.Execf(`firewall-cmd --list-rich-rules`)
if err != nil { // 可能防火墙已关闭等
return true, nil
}
if !strings.Contains(out, `rule protocol value="icmp" drop`) {
@@ -73,7 +74,14 @@ func (r *safeRepo) GetPingStatus() (bool, error) {
}
func (r *safeRepo) UpdatePingStatus(status bool) error {
var err error
fw, err := firewall.NewFirewall().Status()
if err != nil {
return err
}
if !fw {
return fmt.Errorf("failed to update ping status: firewalld is not running")
}
if status {
_, err = shell.Execf(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
} else {
+333 -121
View File
@@ -5,27 +5,32 @@ import (
"errors"
"fmt"
"path/filepath"
"time"
"slices"
"github.com/go-rat/utils/hash"
"github.com/goccy/go-yaml"
"github.com/gookit/color"
"github.com/spf13/cast"
"gopkg.in/yaml.v3"
"gorm.io/gorm"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
"github.com/TheTNB/panel/pkg/cert"
"github.com/TheTNB/panel/pkg/firewall"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
)
type settingRepo struct{}
type settingRepo struct {
taskRepo biz.TaskRepo
}
func NewSettingRepo() biz.SettingRepo {
return &settingRepo{}
return &settingRepo{
taskRepo: NewTaskRepo(),
}
}
func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, error) {
@@ -43,6 +48,21 @@ func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, e
return setting.Value, nil
}
func (r *settingRepo) GetBool(key biz.SettingKey, defaultValue ...bool) (bool, error) {
setting := new(biz.Setting)
if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return false, err
}
}
if setting.Value == "" && len(defaultValue) > 0 {
return defaultValue[0], nil
}
return cast.ToBool(setting.Value), nil
}
func (r *settingRepo) Set(key biz.SettingKey, value string) error {
setting := new(biz.Setting)
if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil {
@@ -66,16 +86,20 @@ func (r *settingRepo) Delete(key biz.SettingKey) error {
}
func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSetting, error) {
name := new(biz.Setting)
if err := app.Orm.Where("key = ?", biz.SettingKeyName).First(name).Error; err != nil {
name, err := r.Get(biz.SettingKeyName)
if err != nil {
return nil, err
}
websitePath := new(biz.Setting)
if err := app.Orm.Where("key = ?", biz.SettingKeyWebsitePath).First(websitePath).Error; err != nil {
offlineMode, err := r.Get(biz.SettingKeyOfflineMode)
if err != nil {
return nil, err
}
backupPath := new(biz.Setting)
if err := app.Orm.Where("key = ?", biz.SettingKeyBackupPath).First(backupPath).Error; err != nil {
websitePath, err := r.Get(biz.SettingKeyWebsitePath)
if err != nil {
return nil, err
}
backupPath, err := r.Get(biz.SettingKeyBackupPath)
if err != nil {
return nil, err
}
@@ -85,7 +109,7 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
return nil, err
}
cert, err := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
crt, err := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
if err != nil {
return nil, err
}
@@ -95,16 +119,17 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
}
return &request.PanelSetting{
Name: name.Value,
Name: name,
Locale: app.Conf.String("app.locale"),
Entrance: app.Conf.String("http.entrance"),
WebsitePath: websitePath.Value,
BackupPath: backupPath.Value,
OfflineMode: cast.ToBool(offlineMode),
WebsitePath: websitePath,
BackupPath: backupPath,
Username: user.Username,
Email: user.Email,
Port: app.Conf.Int("http.port"),
HTTPS: app.Conf.Bool("http.tls"),
Cert: cert,
Cert: crt,
Key: key,
}, nil
}
@@ -113,6 +138,9 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
if err := r.Set(biz.SettingKeyName, setting.Name); err != nil {
return false, err
}
if err := r.Set(biz.SettingKeyOfflineMode, cast.ToString(setting.OfflineMode)); err != nil {
return false, err
}
if err := r.Set(biz.SettingKeyWebsitePath, setting.WebsitePath); err != nil {
return false, err
}
@@ -120,43 +148,10 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
return false, err
}
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), setting.Cert, 0644); err != nil {
return false, err
}
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), setting.Key, 0644); err != nil {
return false, err
}
restartFlag := false
config := new(types.PanelConfig)
cm := yaml.CommentMap{}
raw, err := io.Read("/usr/local/etc/panel/config.yml")
if err != nil {
return false, err
}
if err = yaml.UnmarshalWithOptions([]byte(raw), config, yaml.CommentToMap(cm)); err != nil {
return false, err
}
config.App.Locale = setting.Locale
config.HTTP.Port = setting.Port
config.HTTP.Entrance = setting.Entrance
config.HTTP.TLS = setting.HTTPS
encoded, err := yaml.MarshalWithOptions(config, yaml.WithComment(cm))
if err != nil {
return false, err
}
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0644); err != nil {
return false, err
}
if raw != string(encoded) {
restartFlag = true
}
// 用户
user := new(biz.User)
userID := cast.ToUint(ctx.Value("user_id"))
if err = app.Orm.Where("id = ?", userID).First(user).Error; err != nil {
if err := app.Orm.Where("id = ?", userID).First(user).Error; err != nil {
return false, err
}
@@ -169,7 +164,73 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
}
user.Password = value
}
if err = app.Orm.Save(user).Error; err != nil {
if err := app.Orm.Save(user).Error; err != nil {
return false, err
}
// 下面是需要需要重启的设置
// 面板HTTPS
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 r.taskRepo.HasRunningTask() {
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
}
restartFlag = true
}
if _, err := cert.ParseCert(setting.Cert); err != nil {
return false, fmt.Errorf("failed to parse certificate: %w", err)
}
if _, err := cert.ParseKey(setting.Key); err != nil {
return false, fmt.Errorf("failed to parse private key: %w", err)
}
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), setting.Cert, 0644); err != nil {
return false, err
}
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), setting.Key, 0644); err != nil {
return false, err
}
// 面板主配置
config := new(types.PanelConfig)
raw, err := io.Read("/usr/local/etc/panel/config.yml")
if err != nil {
return false, err
}
if err = yaml.Unmarshal([]byte(raw), config); err != nil {
return false, err
}
config.App.Locale = setting.Locale
config.HTTP.Port = setting.Port
config.HTTP.Entrance = setting.Entrance
config.HTTP.TLS = setting.HTTPS
// 放行端口
fw := firewall.NewFirewall()
err = fw.Port(firewall.FireInfo{
Type: firewall.TypeNormal,
PortStart: uint(config.HTTP.Port),
PortEnd: uint(config.HTTP.Port),
Direction: firewall.DirectionIn,
Strategy: firewall.StrategyAccept,
}, firewall.OperationAdd)
if err != nil {
return false, err
}
encoded, err := yaml.Marshal(config)
if err != nil {
return false, err
}
if raw != string(encoded) {
if r.taskRepo.HasRunningTask() {
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
}
restartFlag = true
}
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0644); err != nil {
return false, err
}
@@ -177,108 +238,128 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
}
func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
// 预先优化数据库
if err := app.Orm.Exec("VACUUM").Error; err != nil {
return err
}
if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE);").Error; err != nil {
return err
}
name := filepath.Base(url)
color.Greenln("目标版本: ", version)
color.Greenln("下载链接: ", url)
color.Greenln("文件名: ", name)
if app.IsCli {
fmt.Printf("|-目标版本:%s\n", version)
fmt.Printf("|-下载链接:%s\n", url)
fmt.Printf("|-文件名:%s\n", name)
}
color.Greenln("前置检查...")
if app.IsCli {
fmt.Println("|-正在下载...")
}
if _, err := shell.Execf("wget -T 120 -t 3 -O /tmp/%s %s", name, url); err != nil {
return fmt.Errorf("下载失败:%w", err)
}
if _, err := shell.Execf("wget -T 20 -t 3 -O /tmp/%s %s", name+".sha256", checksum); err != nil {
return fmt.Errorf("下载失败:%w", err)
}
if !io.Exists(filepath.Join("/tmp", name)) || !io.Exists(filepath.Join("/tmp", name+".sha256")) {
return errors.New("下载文件检查失败")
}
if app.IsCli {
fmt.Println("|-校验下载文件...")
}
if check, err := shell.Execf("cd /tmp && sha256sum -c %s --ignore-missing", name+".sha256"); check != name+": OK" || err != nil {
return errors.New("下载文件校验失败")
}
if err := io.Remove(filepath.Join("/tmp", name+".sha256")); err != nil {
if app.IsCli {
fmt.Println("|-清理校验文件失败:", err)
}
return fmt.Errorf("清理校验文件失败:%w", err)
}
if app.IsCli {
fmt.Println("|-前置检查...")
}
if io.Exists("/tmp/panel-storage.zip") {
return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败导致的,请排除后重试")
return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败所致,请运行 panel-cli fix 修复后重试")
}
color.Greenln("备份面板数据...")
// 备份面板
if err := io.Compress([]string{filepath.Join(app.Root, "panel")}, filepath.Join(app.Root, fmt.Sprintf("backup/panel/panel-%s.zip", time.Now().Format("20060102150405"))), io.Zip); err != nil {
color.Redln("备份面板失败:", err)
return err
if app.IsCli {
fmt.Println("|-备份面板数据...")
}
if err := io.Compress([]string{filepath.Join(app.Root, "panel/storage")}, "/tmp/panel-storage.zip", io.Zip); err != nil {
color.Redln("备份面板数据失败:", err)
return err
// 备份面板
backup := NewBackupRepo()
if err := backup.Create(biz.BackupTypePanel, ""); err != nil {
if app.IsCli {
fmt.Println("|-备份面板失败:", err)
}
return fmt.Errorf("备份面板失败:%w", 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)
}
if !io.Exists("/tmp/panel-storage.zip") {
return errors.New("已备份面板数据检查失败")
}
color.Greenln("备份完成")
color.Greenln("清理旧版本...")
if app.IsCli {
fmt.Println("|-清理旧版本...")
}
if _, err := shell.Execf("rm -rf %s/panel/*", app.Root); err != nil {
color.Redln("清理旧版本失败:", err)
return err
return fmt.Errorf("清理旧版本失败:%w", err)
}
color.Greenln("清理完成")
color.Greenln("正在下载...")
if _, err := shell.Execf("wget -T 120 -t 3 -O %s/panel/%s %s", app.Root, name, url); err != nil {
color.Redln("下载失败:", err)
return err
if app.IsCli {
fmt.Println("|-解压新版本...")
}
if _, err := shell.Execf("wget -T 20 -t 3 -O %s/panel/%s %s", app.Root, name+".sha256", checksum); err != nil {
color.Redln("下载失败:", err)
return err
}
if !io.Exists(filepath.Join(app.Root, "panel", name)) || !io.Exists(filepath.Join(app.Root, "panel", name+".sha256")) {
return errors.New("下载文件检查失败")
}
color.Greenln("下载完成")
color.Greenln("校验下载文件...")
check, err := shell.Execf("cd %s/panel && sha256sum -c %s --ignore-missing", app.Root, name+".sha256")
if check != name+": OK" || err != nil {
return errors.New("下载文件校验失败")
}
if err = io.Remove(filepath.Join(app.Root, "panel", name+".sha256")); err != nil {
color.Redln("清理校验文件失败:", err)
return err
}
color.Greenln("文件校验完成")
color.Greenln("更新新版本...")
if _, err = shell.Execf("cd %s/panel && unzip -o %s && rm -rf %s", app.Root, name, name); err != nil {
color.Redln("更新失败:", err)
return err
if err := io.UnCompress(filepath.Join("/tmp", name), filepath.Join(app.Root, "panel")); err != nil {
return fmt.Errorf("解压失败:%w", err)
}
if !io.Exists(filepath.Join(app.Root, "panel", "web")) {
return errors.New("更新失败,可能是下载过程中出现了问题")
return errors.New("解压失败,缺失文件")
}
color.Greenln("更新完成")
color.Greenln("恢复面板数据...")
if err = io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
color.Redln("恢复面板数据失败:", err)
return err
if app.IsCli {
fmt.Println("|-恢复面板数据...")
}
if err := io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel", "storage")); err != nil {
return fmt.Errorf("恢复面板数据失败:%w", err)
}
if !io.Exists(filepath.Join(app.Root, "panel/storage/app.db")) {
return errors.New("恢复面板数据失败")
}
color.Greenln("恢复完成")
color.Greenln("运行升级后脚本...")
if _, err = shell.Execf("curl -fsLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
color.Redln("运行面板升级后脚本失败:", err)
return err
if app.IsCli {
fmt.Println("|-运行更新后脚本...")
}
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 {
color.Redln("下载面板服务文件失败:", err)
return err
if _, err := shell.Execf("curl -fsLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
return fmt.Errorf("运行面板更新后脚本失败:%w", err)
}
if _, err = shell.Execf("panel-cli setting write version %s", version); err != nil {
color.Redln("写入面板版本号失败:", err)
return 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)
}
if err = io.Mv(filepath.Join(app.Root, "panel/cli"), "/usr/local/sbin/panel-cli"); err != nil {
color.Redln("移动面板命令行工具失败:", err)
return err
if _, err := shell.Execf("panel-cli setting write version %s", version); err != nil {
return fmt.Errorf("写入面板版本号失败:%w", err)
}
if err := io.Mv(filepath.Join(app.Root, "panel/cli"), "/usr/local/sbin/panel-cli"); err != nil {
return fmt.Errorf("移动面板命令行工具失败:%w", err)
}
color.Greenln("设置面板文件权限...")
if app.IsCli {
fmt.Println("|-设置关键文件权限...")
}
_ = io.Chmod("/usr/local/sbin/panel-cli", 0700)
_ = io.Chmod("/etc/systemd/system/panel.service", 0700)
_ = io.Chmod(filepath.Join(app.Root, "panel"), 0700)
color.Greenln("设置完成")
color.Greenln("升级完成")
if app.IsCli {
fmt.Println("|-更新完成")
}
_, _ = shell.Execf("systemctl daemon-reload")
_ = io.Remove("/tmp/panel-storage.zip")
@@ -287,3 +368,134 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
return nil
}
func (r *settingRepo) FixPanel() error {
if app.IsCli {
fmt.Println("|-开始修复面板...")
}
// 检查关键文件是否正常
flag := false
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
}
if !flag {
return fmt.Errorf("文件正常无需修复,请运行 panel-cli update 更新面板")
}
// 再次确认是否需要修复
if io.Exists("/tmp/panel-storage.zip") {
// 文件齐全情况下只移除临时文件
if io.Exists(filepath.Join(app.Root, "panel", "web")) &&
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)
}
if app.IsCli {
fmt.Println("|-已清理临时文件,请运行 panel-cli update 更新面板")
}
return nil
}
}
// 从备份目录中找最新的备份文件
backup := NewBackupRepo()
list, err := backup.List(biz.BackupTypePanel)
if err != nil {
return err
}
slices.SortFunc(list, func(a *types.BackupFile, b *types.BackupFile) int {
return int(b.Time.Unix() - a.Time.Unix())
})
if len(list) == 0 {
return fmt.Errorf("未找到备份文件,无法自动修复")
}
latest := list[0]
if app.IsCli {
fmt.Printf("|-使用备份文件:%s\n", latest.Name)
}
// 解压备份文件
if app.IsCli {
fmt.Println("|-解压备份文件...")
}
if err = io.Remove("/tmp/panel-fix"); err != nil {
return fmt.Errorf("清理临时目录失败:%w", err)
}
if err = io.UnCompress(latest.Path, "/tmp/panel-fix"); err != nil {
return fmt.Errorf("解压备份文件失败:%w", err)
}
// 移动文件到对应位置
if app.IsCli {
fmt.Println("|-移动备份文件...")
}
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)
}
if err = io.Mv(filepath.Join("/tmp/panel-fix", "panel"), filepath.Join(app.Root)); err != nil {
return fmt.Errorf("移动目录失败:%w", 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)
}
}
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)
}
}
// tmp 目录下如果有 storage 备份,则解压回去
if app.IsCli {
fmt.Println("|-恢复面板数据...")
}
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)
}
if err = io.Remove("/tmp/panel-storage.zip"); err != nil {
return fmt.Errorf("清理临时文件失败:%w", err)
}
}
// 下载服务文件
if !io.Exists("/etc/systemd/system/panel.service") {
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 err
}
}
// 处理权限
if app.IsCli {
fmt.Println("|-设置关键文件权限...")
}
if err = io.Chmod("/usr/local/etc/panel/config.yml", 0600); err != nil {
return err
}
if err = io.Chmod("/etc/systemd/system/panel.service", 0700); err != nil {
return err
}
if err = io.Chmod("/usr/local/sbin/panel-cli", 0700); err != nil {
return err
}
if err = io.Chmod(filepath.Join(app.Root, "panel"), 0700); err != nil {
return err
}
if app.IsCli {
fmt.Println("|-修复完成")
}
tools.RestartPanel()
return nil
}
+66 -31
View File
@@ -1,12 +1,12 @@
package data
import (
"errors"
"github.com/spf13/cast"
"fmt"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/http/request"
pkgssh "github.com/TheTNB/panel/pkg/ssh"
)
type sshRepo struct {
@@ -19,36 +19,71 @@ func NewSSHRepo() biz.SSHRepo {
}
}
func (r *sshRepo) GetInfo() (map[string]any, error) {
host, _ := r.settingRepo.Get(biz.SettingKeySshHost)
port, _ := r.settingRepo.Get(biz.SettingKeySshPort)
user, _ := r.settingRepo.Get(biz.SettingKeySshUser)
password, _ := r.settingRepo.Get(biz.SettingKeySshPassword)
if len(host) == 0 || len(user) == 0 || len(password) == 0 {
return nil, errors.New("SSH 配置不完整")
}
return map[string]any{
"host": host,
"port": cast.ToInt(port),
"user": user,
"password": password,
}, nil
func (r *sshRepo) List(page, limit uint) ([]*biz.SSH, int64, error) {
var ssh []*biz.SSH
var total int64
err := app.Orm.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
}
func (r *sshRepo) UpdateInfo(req *request.SSHUpdateInfo) error {
if err := r.settingRepo.Set(biz.SettingKeySshHost, req.Host); err != nil {
return err
}
if err := r.settingRepo.Set(biz.SettingKeySshPort, cast.ToString(req.Port)); err != nil {
return err
}
if err := r.settingRepo.Set(biz.SettingKeySshUser, req.User); err != nil {
return err
}
if err := r.settingRepo.Set(biz.SettingKeySshPassword, req.Password); err != nil {
return err
func (r *sshRepo) Get(id uint) (*biz.SSH, error) {
ssh := new(biz.SSH)
if err := app.Orm.Where("id = ?", id).First(ssh).Error; err != nil {
return nil, err
}
return nil
return ssh, nil
}
func (r *sshRepo) Create(req *request.SSHCreate) error {
conf := pkgssh.ClientConfig{
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
User: req.User,
Password: req.Password,
Key: req.Key,
}
_, err := pkgssh.NewSSHClient(conf)
if err != nil {
return fmt.Errorf("failed to check ssh connection: %v", err)
}
ssh := &biz.SSH{
Name: req.Name,
Host: req.Host,
Port: req.Port,
Config: conf,
Remark: req.Remark,
}
return app.Orm.Create(ssh).Error
}
func (r *sshRepo) Update(req *request.SSHUpdate) error {
conf := pkgssh.ClientConfig{
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
User: req.User,
Password: req.Password,
Key: req.Key,
}
_, err := pkgssh.NewSSHClient(conf)
if err != nil {
return fmt.Errorf("failed to check ssh connection: %v", err)
}
ssh := &biz.SSH{
ID: req.ID,
Name: req.Name,
Host: req.Host,
Port: req.Port,
Config: conf,
Remark: req.Remark,
}
return app.Orm.Model(ssh).Where("id = ?", req.ID).Select("*").Updates(ssh).Error
}
func (r *sshRepo) Delete(id uint) error {
return app.Orm.Delete(&biz.SSH{}, id).Error
}
+35 -6
View File
@@ -1,14 +1,22 @@
package data
import (
"fmt"
"log/slog"
"sync"
"github.com/TheTNB/panel/internal/app"
"github.com/TheTNB/panel/internal/biz"
"github.com/TheTNB/panel/internal/queuejob"
)
var taskDispatchOnce sync.Once
type taskRepo struct{}
func NewTaskRepo() biz.TaskRepo {
task := &taskRepo{}
taskDispatchOnce.Do(task.DispatchWaiting)
return &taskRepo{}
}
@@ -40,25 +48,46 @@ func (r *taskRepo) UpdateStatus(id uint, status biz.TaskStatus) error {
}
func (r *taskRepo) Push(task *biz.Task) error {
var count int64
if err := app.Orm.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")
}
if err := app.Orm.Create(task).Error; err != nil {
return err
}
return app.Queue.Push(queuejob.NewProcessTask(r), []any{
task.ID,
})
}
func (r *taskRepo) DispatchWaiting() error {
func (r *taskRepo) DispatchWaiting() {
// cli下不处理
if app.IsCli {
return
}
if err := app.Orm.Model(&biz.Task{}).Where("status = ?", biz.TaskStatusRunning).Update("status", biz.TaskStatusFailed).Error; err != nil {
app.Logger.Warn("failed to mark running tasks as failed", slog.Any("err", err))
return
}
var tasks []biz.Task
if err := app.Orm.Where("status = ?", biz.TaskStatusWaiting).Find(&tasks).Error; err != nil {
return err
app.Logger.Warn("failed to get pending tasks", slog.Any("err", err))
return
}
for _, task := range tasks {
if err := r.Push(&task); err != nil {
return err
if err := app.Queue.Push(queuejob.NewProcessTask(r), []any{
task.ID,
}); err != nil {
app.Logger.Warn("failed to push task", slog.Any("err", err))
return
}
}
return nil
}
+417 -431
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2,8 +2,8 @@ package embed
import "embed"
//go:embed frontend/*
//go:embed all:frontend/*
var PublicFS embed.FS
//go:embed website/*
//go:embed all:website/*
var WebsiteFS embed.FS
+7 -2
View File
@@ -1,10 +1,12 @@
package middleware
import (
"log/slog"
"net/http"
"github.com/go-chi/chi/v5/middleware"
sessionmiddleware "github.com/go-rat/sessions/middleware"
"github.com/golang-cz/httplog"
"github.com/TheTNB/panel/internal/app"
)
@@ -16,9 +18,12 @@ func GlobalMiddleware() []func(http.Handler) http.Handler {
//middleware.SupressNotFound(app.Http),// bug https://github.com/go-chi/chi/pull/940
middleware.CleanPath,
middleware.StripSlashes,
middleware.Logger,
middleware.Recoverer,
middleware.Compress(5),
httplog.RequestLogger(app.Logger, &httplog.Options{
Level: slog.LevelInfo,
LogRequestHeaders: []string{"User-Agent"},
}),
middleware.Recoverer,
Entrance,
Status,
MustInstall,

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