Compare commits

...

468 Commits

Author SHA1 Message Date
耗子 5b393819ba fix: optimize ci 2024-07-13 01:24:57 +08:00
耗子 f807d0cd1a fix: embed目录错误 2024-07-13 01:20:41 +08:00
耗子 0d6f0f317d fix: embed目录错误 2024-07-13 01:18:03 +08:00
耗子 6bad7f4b5b refactor: 重构入口和前端加载逻辑 2024-07-13 01:17:24 +08:00
耗子 7a7134b26a chore: 更新框架 2024-07-12 22:55:11 +08:00
耗子 28cf7098ff feat: 发布v2.2.22 2024-07-12 18:20:56 +08:00
耗子 b56979147e feat: 发布v2.2.21 2024-07-12 16:51:39 +08:00
耗子 2ec0d9e844 Merge remote-tracking branch 'origin/main' 2024-07-12 16:32:54 +08:00
耗子 66641a626e feat: 更新框架 2024-07-12 16:32:47 +08:00
renovate[bot] 95bfd6843b chore(deps): Update module gorm.io/gorm to v1.25.11 (#139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-11 15:46:32 +00:00
耗子 7e28a34b7f refactor: 重构filter标签 2024-07-11 16:54:34 +08:00
耗子 b2152edeeb feat: session支持刷新 2024-07-11 14:52:16 +08:00
耗子 00815979d2 feat: 发布v2.2.20 2024-07-11 02:59:33 +08:00
耗子 c4e01b37b7 refactor: 使用session鉴权 2024-07-11 02:51:49 +08:00
耗子 7907345521 feat: 更新依赖 2024-07-11 01:08:37 +08:00
耗子 8f32d6cdb0 feat: 优化版本号处理 2024-07-08 04:56:00 +08:00
耗子 c3582903bf fix: CVE-2024-0406 2024-07-08 04:35:52 +08:00
renovate[bot] 7d25d2f5aa chore(deps): Update module golang.org/x/net to v0.27.0 2024-07-05 20:03:32 +00:00
renovate[bot] 46fc7b1c6a chore(deps): Update module golang.org/x/crypto to v0.25.0 2024-07-05 15:11:03 +00:00
耗子 34ac62d1ae fix: lint 2024-07-03 02:03:58 +08:00
耗子 30e78ccf46 feat: 发布v2.2.19 2024-07-03 02:02:38 +08:00
耗子 22f4cc7fcb feat: 支持腾讯云 2024-07-03 01:45:25 +08:00
耗子 1771ae682d Merge remote-tracking branch 'origin/main' 2024-07-02 21:07:19 +08:00
耗子 d1beb60ffd fix: service文件错误 2024-07-02 21:06:57 +08:00
renovate[bot] cba5f86667 chore(deps): Update module github.com/docker/docker to v27.0.3+incompatible 2024-07-02 10:03:44 +00:00
耗子 e1d9832ce0 fix: lint 2024-07-01 15:49:24 +08:00
耗子 6278535902 fix: lint 2024-06-28 23:40:59 +08:00
耗子 32fb87243b feat: 部分软件增加安装校验 2024-06-28 16:41:57 +08:00
耗子 5f85e7fbe0 feat: 添加PostgreSQL的操作方法 2024-06-28 04:01:46 +08:00
耗子 c36104a075 feat: 去掉mysql命令的路径 2024-06-28 03:25:59 +08:00
耗子 583f5d13c7 feat: 优化安装脚本 2024-06-28 03:14:26 +08:00
耗子 b453738daf fix: docker的弃用警告 2024-06-28 03:05:46 +08:00
耗子 98431fb4b2 feat: 发布v2.2.18 2024-06-28 02:53:45 +08:00
耗子 4d76e9286d feat: 删除网站支持删除数据库 2024-06-28 02:52:31 +08:00
耗子 c70758339f fix: build 2024-06-28 01:58:43 +08:00
耗子 ff524cdb0b feat: update dependencies 2024-06-28 01:32:32 +08:00
耗子 73c95d880a Merge remote-tracking branch 'refs/remotes/origin/renovate/github.com-docker-docker-27.x' 2024-06-28 01:28:31 +08:00
耗子 8a1d8c0b05 fix: 优化数据库正则 2024-06-28 01:20:37 +08:00
耗子 2251175906 docs: update readme 2024-06-27 19:13:45 +08:00
机器人 69edde98c8 chore(deps): Update module github.com/docker/docker to v27 2024-06-27 04:07:08 +00:00
耗子 29a3db640d fix: 去掉错误的参数 2024-06-25 18:43:55 +08:00
耗子 ee87bf65b4 feat: 调整部分参数 2024-06-25 18:42:00 +08:00
耗子 aa080dab15 feat: 发布v2.2.17 2024-06-25 03:37:37 +08:00
耗子 b4e5498b48 feat: OpenResty支持返回具体错误 2024-06-25 03:35:35 +08:00
耗子 63838bffa1 feat: phpMyAdmin支持编辑配置 2024-06-25 03:07:38 +08:00
耗子 fa41b18ced fix: 优化查询 2024-06-25 02:51:34 +08:00
耗子 31bd15217b feat: 添加日志记录中间件 2024-06-25 02:38:13 +08:00
耗子 e6b6f06706 feat: 尝试优化内存 2024-06-25 02:08:13 +08:00
耗子 1b836297bf feat: 尝试优化内存 2024-06-25 01:52:41 +08:00
耗子 96bea2cf90 fix: 去掉无用的检查 2024-06-25 01:23:01 +08:00
耗子 8aff513284 fix: 禁止80端口使用HTTPS 2024-06-25 01:13:24 +08:00
耗子 368e41a512 feat: 发布v2.2.16 2024-06-25 01:01:07 +08:00
耗子 924fdee7c5 feat: 支持设置HTTPS端口 2024-06-25 01:00:08 +08:00
耗子 438ba1db1e fix: lint 2024-06-24 23:50:53 +08:00
耗子 d4e52a2d87 feat: ZeroSSL支持自动获取EAB 2024-06-24 23:46:13 +08:00
耗子 4432a12c7a feat: 临时性手段阻止任务重复添加 2024-06-24 23:28:35 +08:00
耗子 a75e36e447 feat: 优化任务 2024-06-24 23:10:39 +08:00
耗子 05674ee04e feat: 优化迁移 2024-06-24 22:53:43 +08:00
耗子 09703761eb fix(文件): 权限设置错误 2024-06-24 22:20:41 +08:00
耗子 dfa14c3fd4 refactor: MySQL改密逻辑 2024-06-24 21:52:46 +08:00
耗子 7b80f034ea feat: 安装前先清理文件 2024-06-24 19:46:43 +08:00
耗子 71923093fd feat(文件): 解压不再判断路径存在 2024-06-24 19:11:01 +08:00
耗子 aae3cf0518 fix: 统一格式 2024-06-24 03:46:21 +08:00
耗子 ed22e79552 feat: 防止密码爆破攻击 2024-06-24 03:45:38 +08:00
耗子 15dd5bb718 feat: 发布v2.2.15 2024-06-23 05:09:30 +08:00
耗子 0e7af2914e feat: 发布v2.2.14 2024-06-23 04:52:18 +08:00
耗子 7ec378829b feat: 优化https证书签发 2024-06-23 04:44:14 +08:00
耗子 f2e8ff86c6 feat: 发布v2.2.13 2024-06-23 03:19:39 +08:00
耗子 eed03d644f fix: test 2024-06-23 03:19:13 +08:00
耗子 d9be877710 feat: 发布v2.2.12 2024-06-23 03:03:38 +08:00
耗子 10439d7d97 feat: 优化证书信息获取 2024-06-23 02:42:01 +08:00
耗子 55eaa26970 feat: 添加面板HTTPS接口 2024-06-23 02:35:25 +08:00
耗子 0d769d3400 fix: lint 2024-06-23 01:46:14 +08:00
耗子 7ff084e5a8 refactor: 重构types包 2024-06-23 01:32:45 +08:00
耗子 89e133c626 refactor: 重构str包 2024-06-23 01:12:51 +08:00
耗子 c876f4c484 refactor: 重构os包 2024-06-23 01:08:07 +08:00
耗子 85c38e5bc5 refactor: 重构io包 2024-06-23 00:59:07 +08:00
耗子 6bd07251fc refactor: 重构systemctl包 2024-06-23 00:48:37 +08:00
耗子 11d6b40dce refactor: 重构shell命令执行 2024-06-23 00:44:14 +08:00
耗子 aa9b55d1eb feat: bump v2.2.11 2024-06-19 02:06:58 +08:00
耗子 2e97812776 fix: 修复版本号比较错误 2024-06-19 02:06:39 +08:00
耗子 e0a7ada6d5 feat(监控): 去掉cpu型号信息 2024-06-19 01:58:40 +08:00
耗子 d48963ab50 fix: 由于v2.2.8迁移问题导致更新脚本可能未运行 2024-06-19 01:20:48 +08:00
耗子 59bfed66dc chore: 发布v2.2.10 2024-06-19 01:18:38 +08:00
耗子 5c0bc8a1b7 feat: 安装后删除安装脚本和checksum 2024-06-19 01:15:00 +08:00
耗子 ae9e39d0fe feat: 优化分页函数 2024-06-19 00:47:57 +08:00
耗子 49b72aa656 feat: 一些细节优化 2024-06-18 13:19:28 +08:00
耗子 e0f642cbff feat: 一些细节优化 2024-06-18 12:58:06 +08:00
耗子 eb2c6cab81 feat: 优化防火墙启停流程 2024-06-18 10:09:53 +08:00
耗子 0a3d3828c8 feat: 优化验证器使用 2024-06-18 03:41:34 +08:00
耗子 46b431de30 fix: 一些小问题 2024-06-18 03:30:59 +08:00
耗子 8a5b46ad7e fix(容器): 分页为空时不返回null 2024-06-18 03:13:39 +08:00
耗子 5bd14946de refactor: 重构postgresql插件 2024-06-18 03:04:37 +08:00
耗子 f6c091c725 feat: 更新文档 2024-06-18 01:35:05 +08:00
耗子 f4dc94ad9a feat: 更新网站文档 2024-06-18 01:32:04 +08:00
耗子 8bcc0c0c05 feat: 更新openresty插件文档 2024-06-18 01:29:44 +08:00
耗子 0be04e1859 feat: 更新php插件文档 2024-06-18 01:26:47 +08:00
耗子 bec9336f2f feat: 重构php插件 2024-06-18 01:20:24 +08:00
耗子 0be4ce9891 revert: 加回must_install中间件 2024-06-18 00:48:48 +08:00
耗子 634a9a3219 feat: 优化编译线程数计算算法 2024-06-18 00:32:47 +08:00
耗子 1f2a32f883 feat: 检查接口返回插件名 2024-06-18 00:28:35 +08:00
耗子 32a56aa2dc feat: 去掉must install中间件和提供新接口检查插件安装 2024-06-17 22:03:38 +08:00
耗子 8185a69354 feat: optimize mysql api 2024-06-17 21:48:28 +08:00
耗子 d41862f63b fix: 自启动podman socket 2024-06-16 04:38:28 +08:00
耗子 20e5122425 fix: lint 2024-06-16 04:20:58 +08:00
耗子 17bb8ad62b fix: debian下缺失podman存储配置 2024-06-16 04:20:39 +08:00
耗子 43624de2e4 feat: 优化自动迁移 2024-06-16 03:55:59 +08:00
耗子 cc6a45b127 feat: 修改模型 2024-06-14 22:52:05 +08:00
耗子 7a4a71f0a0 fix: tests 2024-06-14 21:03:15 +08:00
耗子 27cbb92a06 fix: build 2024-06-14 21:00:12 +08:00
耗子 5ddbdfc5df fix: lint 2024-06-14 19:58:49 +08:00
耗子 b3b1b0971f feat: use new migrate 2024-06-14 19:56:44 +08:00
耗子 9caad365e9 Merge remote-tracking branch 'origin/main' 2024-06-14 19:49:33 +08:00
耗子 5b54232e16 refactor: 移动pureftpd类型定义 2024-06-14 19:49:23 +08:00
机器人 a510bdb05d Merge branch 'renovate/github.com-gorilla-websocket-1.x' into 'main'
chore(deps): Update module github.com/gorilla/websocket to v1.5.3

See merge request opensource/panel!77
2024-06-14 04:12:54 +00:00
机器人 50a8b16832 chore(deps): Update module github.com/gorilla/websocket to v1.5.3 2024-06-14 04:07:55 +00:00
耗子 2f56241da1 refactor: 移动pureftpd类型定义 2024-06-14 11:30:30 +08:00
耗子 8557259b53 feat: 移除部分软件服务接口 2024-06-14 11:28:02 +08:00
耗子 1d7ea0280a feat: 移除部分软件服务接口 2024-06-14 11:10:52 +08:00
耗子 10145da2f9 feat: 移除部分软件服务接口 2024-06-14 10:59:22 +08:00
耗子 6ba7d01875 feat: 添加服务操作接口 2024-06-14 10:33:51 +08:00
耗子 763fa7379d Merge remote-tracking branch 'origin/renovate/goreleaser-goreleaser-action-6.x' 2024-06-14 02:44:56 +08:00
耗子 a36eaf7e9a feat: 自启动podman-restart 2024-06-13 02:38:29 +08:00
耗子 ec10cd30a5 feat: 发布v2.2.8 2024-06-12 01:37:40 +08:00
耗子 847fae5caa feat: 名称统一为耗子面板 2024-06-12 01:36:35 +08:00
耗子 b3b8c22634 feat: 名称统一为耗子面板 2024-06-12 01:32:11 +08:00
耗子 5f504d49ef feat: 创建容器后自动启动 2024-06-12 01:09:09 +08:00
耗子 44045da4eb feat: 添加默认404页面 2024-06-11 03:36:11 +08:00
耗子 baf90c0602 feat: 发布v2.2.7 2024-06-11 03:19:32 +08:00
耗子 f7e67c3797 fix: chmod权限问题 2024-06-11 03:19:06 +08:00
耗子 d7a79fa53e feat: 发布v2.2.6 2024-06-11 02:07:52 +08:00
耗子 b9b3b9fedf feat: 不再默认安装podman 2024-06-11 02:03:46 +08:00
耗子 e27940a71b feat: Podman插件 2024-06-11 01:26:34 +08:00
耗子 aa22933b36 Merge remote-tracking branch 'origin/main' 2024-06-10 01:54:33 +08:00
耗子 a1dceaaa82 feat: 添加Podman安装脚本 2024-06-10 01:53:28 +08:00
机器人 ebd5d37e78 Merge branch 'renovate/github.com-gorilla-websocket-1.x' into 'main'
chore(deps): Update module github.com/gorilla/websocket to v1.5.2

See merge request opensource/panel!76
2024-06-08 16:12:13 +00:00
机器人 107d2ec658 chore(deps): Update module github.com/gorilla/websocket to v1.5.2 2024-06-08 16:06:59 +00:00
耗子 3a70d80bb5 feat: 发布v2.2.5 2024-06-08 19:30:30 +08:00
耗子 7c58d8dcce feat: 发布v2.2.5 2024-06-08 19:30:21 +08:00
耗子 bde93b488c fix: lint 2024-06-08 19:27:53 +08:00
耗子 77aa4bea34 feat: 添加Gitea 2024-06-08 19:26:23 +08:00
机器人 18de16047d chore(deps): Update goreleaser/goreleaser-action action to v6 2024-06-08 10:05:02 +00:00
耗子 f7ba602a50 fix: lint 2024-06-08 17:58:19 +08:00
耗子 3101486a44 fix: lint 2024-06-08 17:49:40 +08:00
耗子 b51a8e1d03 chore: 更新依赖 2024-06-08 17:37:04 +08:00
耗子 83f58e0a2e feat: 发布v2.2.4 2024-06-08 16:09:59 +08:00
耗子 e941ab6661 feat: 更新redis和php版本 2024-06-08 15:59:01 +08:00
耗子 688801eebb fix: lint 2024-06-08 15:37:39 +08:00
耗子 8ad42f8c0e fix: frp 相关接口 2024-06-08 15:26:57 +08:00
耗子 02c8809785 Merge remote-tracking branch 'origin/main' 2024-06-08 14:48:23 +08:00
耗子 fd0e43fb26 fix: 容器列表空切片 2024-06-08 14:48:15 +08:00
机器人 96ec795096 Merge branch 'renovate/github.com-docker-docker-26.x' into 'main'
chore(deps): Update module github.com/docker/docker to v26.1.4+incompatible

See merge request opensource/panel!75
2024-06-05 22:18:50 +00:00
机器人 5ea191b550 Merge branch 'renovate/golang.org-x-net-0.x' into 'main'
chore(deps): Update module golang.org/x/net to v0.26.0

See merge request opensource/panel!73
2024-06-05 22:16:57 +00:00
机器人 c37cc3bd2c chore(deps): Update module golang.org/x/net to v0.26.0 2024-06-05 22:11:16 +00:00
机器人 110f305f16 chore(deps): Update module github.com/docker/docker to v26.1.4+incompatible 2024-06-05 22:10:40 +00:00
机器人 179b6ff83e Merge branch 'renovate/golang.org-x-crypto-0.x' into 'main'
chore(deps): Update module golang.org/x/crypto to v0.24.0

See merge request opensource/panel!72
2024-06-05 16:13:59 +00:00
机器人 0da1b0b565 chore(deps): Update module golang.org/x/crypto to v0.24.0 2024-06-05 16:09:16 +00:00
耗子 0da3f7b8e0 feat: 切换镜像源 2024-06-05 19:58:50 +08:00
耗子 c6f9a585b0 fix: lint 2024-06-04 22:47:47 +08:00
耗子 686bdd9ae2 fix: frp安装 2024-06-03 02:58:39 +08:00
耗子 e3a22f924e fix: lint 2024-06-03 02:07:47 +08:00
耗子 0c3c47361b feat: frp管理器 2024-06-03 02:07:21 +08:00
耗子 c3f9f56f11 feat: 调整定时任务时间 2024-06-03 01:27:40 +08:00
耗子 76e992cc9e chore: fix lint 2024-05-30 02:27:35 +08:00
耗子 73cc5a35ad refactor: move types 2024-05-30 01:58:17 +08:00
耗子 873520e9f4 chore: update framework 2024-05-30 01:44:44 +08:00
耗子 af3254c11a docs: update README 2024-05-30 00:30:37 +08:00
耗子 5575d34ab4 fix: test 2024-05-29 21:47:35 +08:00
耗子 a995c56f1b fix: lint 2024-05-29 21:38:59 +08:00
耗子 1fbea148e3 refactor: 重命名软件包名 2024-05-29 21:31:01 +08:00
耗子 ca504e1acc feat: optimize task 2024-05-29 21:19:39 +08:00
耗子 18fe84117d feat: 使用新队列 2024-05-29 19:20:06 +08:00
耗子 9eb9a2c7d6 fix(安全): SSH端口类型 2024-05-26 14:19:56 +08:00
耗子 de7d50ce51 feat: optimize info api 2024-05-26 14:14:48 +08:00
耗子 6e0d93257f feat: add language api 2024-05-26 13:37:58 +08:00
耗子 96806d4e94 feat: bump v2.2.3 2024-05-24 00:05:18 +08:00
耗子 3de78c95bf ci: 优化upx压缩速度 2024-05-23 23:52:55 +08:00
耗子 367348e688 feat: bump v2.2.2 2024-05-23 23:14:59 +08:00
耗子 07dfcd778c feat: bump v2.2.1 2024-05-23 22:27:43 +08:00
耗子 789d5e7ddd fix: test 2024-05-23 22:04:13 +08:00
耗子 1102aba9a3 fix: test 2024-05-23 21:04:20 +08:00
耗子 fddb076136 fix: #93 2024-05-23 20:33:16 +08:00
耗子 b1ff8fff3d feat(文件): 优化chmod使用系统命令 2024-05-23 20:27:09 +08:00
耗子 5cd68fcdc3 fix(文件): 统一copy的字段 2024-05-21 21:32:54 +08:00
耗子 3a5ff9179a fix: 获取公网IP 2024-05-21 02:01:14 +08:00
耗子 fa7c54ada5 feat: 优化打包大小 2024-05-20 02:16:14 +08:00
耗子 94047174d1 feat: 优化打包大小 2024-05-20 02:04:04 +08:00
耗子 305777f65a feat: bump version v2.2.0 2024-05-20 01:26:45 +08:00
耗子 d56d3ec1dd feat: 优化路径正则 2024-05-20 01:13:33 +08:00
耗子 8ac8da7914 feat: 优化路径正则 2024-05-20 00:56:39 +08:00
耗子 5d56663b69 fix: 脚本文件权限 2024-05-19 20:31:33 +08:00
耗子 7ff574cb92 fix: OpenResty pcre2 版本号 2024-05-19 16:09:34 +08:00
耗子 00b4153a28 feat: 添加软链接返回 2024-05-19 04:27:17 +08:00
耗子 9412b31279 feat: 助手函数更新 2024-05-19 04:21:57 +08:00
耗子 bbf04c91cf Merge remote-tracking branch 'origin/main' 2024-05-19 04:18:51 +08:00
耗子 252456a1cd feat: 文件管理分页 2024-05-19 04:18:42 +08:00
机器人 8de68b317d Merge branch 'renovate/github.com-imroc-req-v3-3.x' into 'main'
chore(deps): Update module github.com/imroc/req/v3 to v3.43.5

See merge request opensource/panel!71
2024-05-16 16:19:01 +00:00
机器人 8bf273e1fd Merge branch 'renovate/github.com-docker-docker-26.x' into 'main'
chore(deps): Update module github.com/docker/docker to v26.1.3+incompatible

See merge request opensource/panel!70
2024-05-16 16:17:48 +00:00
机器人 32111ecd7a chore(deps): Update module github.com/imroc/req/v3 to v3.43.5 2024-05-16 16:08:19 +00:00
机器人 82b8a32133 chore(deps): Update module github.com/docker/docker to v26.1.3+incompatible 2024-05-16 16:07:37 +00:00
耗子 b82c644d81 feat: MySQL 8.4 支持 2024-05-16 00:18:01 +08:00
耗子 06bf1b0604 feat: 部分软件使用 7z 格式 2024-05-15 22:48:17 +08:00
耗子 7b29e4546a feat: 替换新的下载节点 2024-05-15 21:53:04 +08:00
耗子 ae6b354a78 feat: MySQL 安装脚本支持 8.4 2024-05-15 02:56:39 +08:00
耗子 d96c616c61 feat: start rsyslog after install 2024-05-10 22:41:43 +08:00
耗子 213698dd2a feat: install rsyslog in el 9 2024-05-10 22:38:35 +08:00
耗子 39b3c74684 Merge remote-tracking branch 'refs/remotes/origin/renovate/golangci-golangci-lint-action-6.x' 2024-05-10 22:37:58 +08:00
机器人 b3a1af1001 Merge branch 'renovate/github.com-docker-docker-26.x' into 'main'
chore(deps): Update module github.com/docker/docker to v26.1.2+incompatible

See merge request opensource/panel!69
2024-05-09 16:18:56 +00:00
机器人 91b244276a chore(deps): Update module github.com/docker/docker to v26.1.2+incompatible 2024-05-09 16:09:17 +00:00
机器人 b1eefd062d Merge branch 'renovate/github.com-imroc-req-v3-3.x' into 'main'
chore(deps): Update module github.com/imroc/req/v3 to v3.43.4

See merge request opensource/panel!68
2024-05-08 10:22:09 +00:00
机器人 2edf6748e7 chore(deps): Update module github.com/imroc/req/v3 to v3.43.4 2024-05-08 10:07:04 +00:00
机器人 eac69b8dab chore(deps): Update golangci/golangci-lint-action action to v6 2024-05-07 04:07:04 +00:00
机器人 bb7b641193 Merge branch 'renovate/golang.org-x-net-0.x' into 'main'
chore(deps): Update module golang.org/x/net to v0.25.0

See merge request opensource/panel!66
2024-05-06 22:27:43 +00:00
机器人 3b0aee1967 Merge branch 'renovate/github.com-mholt-acmez-v2-2.x' into 'main'
chore(deps): Update module github.com/mholt/acmez/v2 to v2.0.1

See merge request opensource/panel!65
2024-05-06 22:24:19 +00:00
机器人 2ae7f3e458 chore(deps): Update module golang.org/x/net to v0.25.0 2024-05-06 22:10:22 +00:00
机器人 ee0442d159 chore(deps): Update module github.com/mholt/acmez/v2 to v2.0.1 2024-05-06 22:09:55 +00:00
机器人 363b7a55be Merge branch 'renovate/golang.org-x-crypto-0.x' into 'main'
chore(deps): Update module golang.org/x/crypto to v0.23.0

See merge request opensource/panel!64
2024-05-06 16:19:58 +00:00
机器人 3437bb2b1f chore(deps): Update module golang.org/x/crypto to v0.23.0 2024-05-06 16:08:16 +00:00
耗子 7e000cddc8 feat: optimize script 2024-05-06 03:27:34 +08:00
机器人 a18652f134 Merge branch 'renovate/github.com-docker-docker-26.x' into 'main'
chore(deps): Update module github.com/docker/docker to v26.1.1+incompatible

See merge request opensource/panel!63
2024-04-30 22:21:04 +00:00
机器人 dcd8bb3dd9 chore(deps): Update module github.com/docker/docker to v26.1.1+incompatible 2024-04-30 22:10:15 +00:00
耗子 d62a280fe9 docs: update README 2024-05-01 02:53:36 +08:00
耗子 c3a7710e6f chore: update dependences 2024-05-01 02:51:32 +08:00
耗子 7f1491f739 Merge remote-tracking branch 'origin/renovate/github.com-mholt-acmez-2.x' 2024-04-30 12:54:56 +08:00
耗子 c96b88548e Merge remote-tracking branch 'refs/remotes/origin/renovate/golangci-golangci-lint-action-5.x' 2024-04-30 12:54:04 +08:00
krishan kumar f62b112e1b Merge pull request #91 from TheTNB/kkumar-gcc/#45
feat: support localization
2024-04-28 18:45:37 +05:30
kkumar-gcc e55e3402e9 finish translation 2024-04-27 16:13:28 +05:30
kkumar-gcc 48b539b31a finish translation for panel.writeSite 2024-04-26 19:05:50 +05:30
kkumar-gcc d35245aa11 finish translation for panel.cutoff 2024-04-26 18:46:41 +05:30
kkumar-gcc 7f46dea052 finish translation for panel.writeMysqlPassword 2024-04-26 18:19:55 +05:30
kkumar-gcc 48ed174dc6 finish translation for panel.deletePlugin 2024-04-26 18:16:26 +05:30
kkumar-gcc a5743f94b2 finish translation for panel.writePlugin 2024-04-26 18:13:13 +05:30
kkumar-gcc 227c5344e3 finish translation for panel.deleteEntrance 2024-04-26 18:08:50 +05:30
kkumar-gcc 4c8a88ea03 finish translation for panel.getInfo 2024-04-26 17:57:51 +05:30
kkumar-gcc 3503aac2f6 finish translation for panel.update 2024-04-26 13:36:11 +05:30
耗子 6beda2ea12 feat: 优化 bbr 检查 2024-04-26 15:44:40 +08:00
机器人 4579bf8fa9 chore(deps): Update golangci/golangci-lint-action action to v5 2024-04-25 04:04:38 +00:00
机器人 002db34b4d Merge branch 'renovate/github.com-bytedance-sonic-1.x' into 'main'
chore(deps): Update module github.com/bytedance/sonic to v1.11.6

See merge request opensource/panel!61
2024-04-24 16:17:16 +00:00
机器人 e276c4f747 chore(deps): Update module github.com/bytedance/sonic to v1.11.6 2024-04-24 16:08:10 +00:00
机器人 f5e6219ca4 chore(deps): Update module github.com/mholt/acmez to v2 2024-04-23 22:08:34 +00:00
机器人 fd92b1d360 Merge branch 'renovate/github.com-docker-docker-26.x' into 'main'
chore(deps): Update module github.com/docker/docker to v26.1.0+incompatible

See merge request opensource/panel!59
2024-04-22 22:18:32 +00:00
机器人 5b8289c0b0 chore(deps): Update module github.com/docker/docker to v26.1.0+incompatible 2024-04-22 22:10:41 +00:00
kkumar-gcc d8fe050607 add translation for panel init command 2024-04-22 22:10:38 +05:30
kkumar-gcc c0310d916f add commands prefix for command translation 2024-04-22 20:01:17 +05:30
kkumar-gcc 77ed2d77e1 setup basic translations 2024-04-22 13:51:53 +05:30
耗子 c331143e9e docs: 更新赞助说明 2024-04-21 20:00:59 +08:00
机器人 71e75312ff Merge branch 'renovate/github.com-docker-docker-26.x' into 'main'
chore(deps): Update module github.com/docker/docker to v26.0.2+incompatible

See merge request opensource/panel!58
2024-04-18 22:16:30 +00:00
机器人 0e5bee4852 chore(deps): Update module github.com/docker/docker to v26.0.2+incompatible 2024-04-18 22:08:42 +00:00
机器人 036ad9222a Merge branch 'renovate/github.com-bytedance-sonic-1.x' into 'main'
chore(deps): Update module github.com/bytedance/sonic to v1.11.5

See merge request opensource/panel!57
2024-04-18 10:20:49 +00:00
机器人 39cd5545c5 chore(deps): Update module github.com/bytedance/sonic to v1.11.5 2024-04-18 10:08:40 +00:00
机器人 4568e60133 Merge branch 'renovate/github.com-bytedance-sonic-1.x' into 'main'
chore(deps): Update module github.com/bytedance/sonic to v1.11.4

See merge request opensource/panel!56
2024-04-17 10:15:52 +00:00
机器人 d2f224eaa6 chore(deps): Update module github.com/bytedance/sonic to v1.11.4 2024-04-17 10:08:10 +00:00
耗子 c77713b3d9 fix: lint 2024-04-17 14:58:46 +08:00
耗子 f1e2ba5c35 feat: 降级 sonic 以减少打包大小 2024-04-17 14:49:48 +08:00
耗子 243a36f41c Merge remote-tracking branch 'github/renovate/github.com-docker-docker-26.x' 2024-04-17 14:45:03 +08:00
机器人 74f772b705 Merge branch 'renovate/github.com-imroc-req-v3-3.x' into 'main'
chore(deps): Update module github.com/imroc/req/v3 to v3.43.3

See merge request opensource/panel!55
2024-04-16 16:23:08 +00:00
机器人 e8f1ed5dec chore(deps): Update module github.com/imroc/req/v3 to v3.43.3 2024-04-16 16:09:32 +00:00
机器人 634d8c643b chore(deps): Update module github.com/docker/docker to v26 2024-04-11 16:07:23 +00:00
机器人 1829b46466 Merge branch 'renovate/github.com-imroc-req-v3-3.x' into 'main'
chore(deps): Update module github.com/imroc/req/v3 to v3.43.2

See merge request opensource/panel!54
2024-04-08 04:18:51 +00:00
机器人 478efa5016 chore(deps): Update module github.com/imroc/req/v3 to v3.43.2 2024-04-08 04:06:37 +00:00
耗子 841e0e351f feat: 在面板定时任务出错时将面板设置为错误状态 2024-04-05 21:12:14 +08:00
耗子 0e9761f975 feat: 优化更新中的输出 2024-04-05 21:08:44 +08:00
机器人 5c067e28b7 Merge branch 'renovate/golang.org-x-net-0.x' into 'main'
chore(deps): Update module golang.org/x/net to v0.24.0

See merge request opensource/panel!53
2024-04-05 04:15:08 +00:00
机器人 0a0e0bcac1 chore(deps): Update module golang.org/x/net to v0.24.0 2024-04-05 04:09:02 +00:00
机器人 c3732b904d Merge branch 'renovate/golang.org-x-crypto-0.x' into 'main'
chore(deps): Update module golang.org/x/crypto to v0.22.0

See merge request opensource/panel!52
2024-04-04 22:20:53 +00:00
机器人 161370ac5a chore(deps): Update module golang.org/x/crypto to v0.22.0 2024-04-04 22:11:01 +00:00
机器人 0cfe585ad5 Merge branch 'renovate/golang.org-x-net-0.x' into 'main'
chore(deps): Update module golang.org/x/net to v0.23.0

See merge request opensource/panel!51
2024-04-03 22:23:25 +00:00
机器人 283d9dbe26 chore(deps): Update module golang.org/x/net to v0.23.0 2024-04-03 22:10:16 +00:00
耗子 45ed3f2ff9 feat: 发布 v2.1.44 2024-03-25 00:38:04 +08:00
耗子 5703e3354f feat: CLI 添加插件和网站的命令 2024-03-24 22:38:22 +08:00
耗子 da2e66b7c6 docs: update README 2024-03-24 17:29:35 +08:00
耗子 8dca4c1cbf fix: 系统工具箱版本号错误 2024-03-24 17:12:08 +08:00
耗子 a295eb9a65 ci: 固定 Go 1.22 版本 2024-03-24 16:28:09 +08:00
耗子 868ee73d51 Merge remote-tracking branch 'origin/main' 2024-03-24 15:55:21 +08:00
耗子 ef7d38f2af feat(证书): 优化签发及部署 2024-03-24 15:55:10 +08:00
机器人 801ff28854 Merge branch 'renovate/go.uber.org-zap-1.x' into 'main'
chore(deps): Update module go.uber.org/zap to v1.27.0

See merge request opensource/panel!50
2024-03-22 22:18:27 +00:00
机器人 e0a401f688 chore(deps): Update module go.uber.org/zap to v1.27.0 2024-03-22 22:10:00 +00:00
耗子 f50f2f7f2b fix: lint 2024-03-23 01:11:36 +08:00
耗子 3df1388326 fix: lint 2024-03-23 01:05:56 +08:00
耗子 5390ef3140 refactor: 移除 lego 2024-03-23 00:50:59 +08:00
耗子 7cb5a3db4b feat: 发布 v2.1.43 2024-03-21 01:05:33 +08:00
耗子 082b0d2755 feat: 发布 v2.1.42 2024-03-21 00:42:23 +08:00
机器人 10ba87b63a Merge branch 'renovate/github.com-docker-go-connections-0.x' into 'main'
chore(deps): Update module github.com/docker/go-connections to v0.5.0

See merge request opensource/panel!48
2024-03-20 04:16:27 +00:00
机器人 ec4c187a11 chore(deps): Update module github.com/docker/go-connections to v0.5.0 2024-03-20 04:10:45 +00:00
机器人 2e250dbb39 Merge branch 'renovate/github.com-docker-docker-25.x' into 'main'
chore(deps): Update module github.com/docker/docker to v25.0.5+incompatible

See merge request opensource/panel!47
2024-03-19 22:16:47 +00:00
机器人 69938cf9bc chore(deps): Update module github.com/docker/docker to v25.0.5+incompatible 2024-03-19 22:09:20 +00:00
耗子 3bc3efe59c fix(容器): 正确解析卷创建时间 2024-03-20 01:41:37 +08:00
耗子 9b6c9484e4 fix(容器): 获取卷列表 panic 2024-03-20 01:33:03 +08:00
耗子 e127572300 feat(容器): 格式化返回 2024-03-17 22:12:43 +08:00
耗子 c757dd9555 fix: lint 2024-03-16 23:10:59 +08:00
耗子 97c3f4c7c7 fix: lint 2024-03-16 23:04:18 +08:00
耗子 1e8c4945d5 refactor: 统一类型结构体至 types 目录 2024-03-16 22:42:36 +08:00
耗子 90ef29acc4 feat: 完善容器管理 2024-03-16 22:37:52 +08:00
耗子 89a6c3733b fix(容器): 镜像拉取 2024-03-16 16:24:43 +08:00
耗子 26d8aacabd feat: 完善容器接口 2024-03-16 15:29:00 +08:00
耗子 e7e5d4b283 fix: panic 2024-03-16 13:02:33 +08:00
耗子 db0e722ea3 feat: 容器部分 2024-03-16 00:15:37 +08:00
耗子 ccd0aa3505 docs: 移除 codecov 2024-03-15 19:48:37 +08:00
耗子 43f19c802f fix: Swow 拓展名称问题 2024-03-15 18:00:51 +08:00
耗子 a86d8a7490 feat: 补全 PHP 拓展 2024-03-15 17:49:06 +08:00
耗子 08d906e27c feat: 添加 PHP FPM 配置的操作接口 2024-03-15 17:22:52 +08:00
耗子 70d65b2b2c feat: 不再强制更新插件 2024-03-15 17:07:55 +08:00
耗子 3de92c9603 refactor: 重构插件结构 2024-03-15 17:02:17 +08:00
耗子 deb9d9fbd2 fix(文件管理): 一堆小问题 2024-03-15 00:39:01 +08:00
耗子 71903c1100 docs: 更新赞助商信息 2024-03-14 18:57:41 +08:00
机器人 9853c1792b Merge branch 'renovate/github.com-imroc-req-v3-3.x' into 'main'
chore(deps): Update module github.com/imroc/req/v3 to v3.43.1

See merge request opensource/panel!46
2024-03-12 10:20:27 +00:00
机器人 18f0257ff2 chore(deps): Update module github.com/imroc/req/v3 to v3.43.1 2024-03-12 10:08:20 +00:00
机器人 0754fcaf6a Merge branch 'renovate/github.com-go-acme-lego-v4-4.x' into 'main'
chore(deps): Update module github.com/go-acme/lego/v4 to v4.16.1

See merge request opensource/panel!45
2024-03-11 04:14:59 +00:00
机器人 ea7bb34be7 chore(deps): Update module github.com/go-acme/lego/v4 to v4.16.1 2024-03-11 04:07:24 +00:00
机器人 383b1652cf Merge branch 'renovate/github.com-go-acme-lego-v4-4.x' into 'main'
chore(deps): Update module github.com/go-acme/lego/v4 to v4.16.0

See merge request opensource/panel!44
2024-03-10 04:15:14 +00:00
机器人 ca36c7c62a chore(deps): Update module github.com/go-acme/lego/v4 to v4.16.0 2024-03-10 04:07:27 +00:00
机器人 c42fc0c70d Merge branch 'renovate/github.com-docker-docker-25.x' into 'main'
chore(deps): Update module github.com/docker/docker to v25.0.4+incompatible

See merge request opensource/panel!43
2024-03-07 16:17:58 +00:00
机器人 a89f5e3ca3 chore(deps): Update module github.com/docker/docker to v25.0.4+incompatible 2024-03-07 16:11:50 +00:00
机器人 64537e1c75 Merge branch 'renovate/github.com-imroc-req-v3-3.x' into 'main'
chore(deps): Update module github.com/imroc/req/v3 to v3.43.0

See merge request opensource/panel!42
2024-03-06 16:21:17 +00:00
机器人 19a57f9951 chore(deps): Update module github.com/imroc/req/v3 to v3.43.0 2024-03-06 16:10:05 +00:00
耗子 95a90db230 feat: 发布 v2.0.41 2024-03-06 03:42:41 +08:00
耗子 5dcfe4126c fix: 修正部分接口未跟随更新状态码 2024-03-06 03:42:15 +08:00
耗子 d3e758f61a fix: 修正 PostgreSQL CLI 下备份提示数据库不存在 2024-03-06 03:38:17 +08:00
耗子 d921939186 feat: 发布 v2.1.40 2024-03-06 03:26:55 +08:00
耗子 b0a5221aed fix: PHP 版本字符串 2024-03-06 03:17:32 +08:00
耗子 dd4db4899d feat: 发布 v2.1.39 2024-03-06 02:38:32 +08:00
耗子 7f0b7b349a feat: 发布 v2.1.39 2024-03-06 02:22:12 +08:00
耗子 2a483bc9cd Merge remote-tracking branch 'origin/main' 2024-03-06 01:27:40 +08:00
耗子 731a1f13a9 feat: 更新一批软件 2024-03-06 01:27:30 +08:00
耗子 95025267aa feat: PHP 8.2 支持 ionCube 2024-03-06 01:04:16 +08:00
机器人 4dd1bd3d46 Merge branch 'renovate/golang.org-x-crypto-0.x' into 'main'
chore(deps): Update module golang.org/x/crypto to v0.21.0

See merge request opensource/panel!41
2024-03-04 22:22:36 +00:00
机器人 633fa61738 chore(deps): Update module golang.org/x/crypto to v0.21.0 2024-03-04 22:11:00 +00:00
耗子 35c998f402 fix: 签发证书不强制 OCSP 装订 2024-03-04 18:34:23 +08:00
机器人 1a82279304 Merge branch 'renovate/github.com-stretchr-testify-1.x' into 'main'
chore(deps): Update module github.com/stretchr/testify to v1.9.0

See merge request opensource/panel!40
2024-03-01 16:21:47 +00:00
机器人 ac4380ef14 chore(deps): Update module github.com/stretchr/testify to v1.9.0 2024-03-01 16:07:42 +00:00
机器人 6b8228a048 Merge branch 'renovate/golang.org-x-crypto-0.x' into 'main'
chore(deps): Update module golang.org/x/crypto to v0.20.0

See merge request opensource/panel!39
2024-02-26 22:17:50 +00:00
机器人 67927eef06 chore(deps): Update module golang.org/x/crypto to v0.20.0 2024-02-26 22:09:26 +00:00
耗子 429a027c79 fix: 磁盘统计信息顺序蹦迪的问题 2024-02-26 16:30:25 +08:00
耗子 284f6fb953 fix: 软件安装日志兼容各种奇葩情况 2024-02-24 00:54:59 +08:00
耗子 8194028a76 fix: lint 2024-02-23 01:57:01 +08:00
耗子 c173f57c6d fix: 在不同的文件系统中移动文件,os.Rename 可能会失败 2024-02-23 01:56:37 +08:00
耗子 639159f150 fix: 修复添加数据库备份数据库名称未正确填充 2024-02-23 01:46:46 +08:00
耗子 89bd5d4b36 feat: 更新 Go 1.22 2024-02-19 23:20:43 +08:00
机器人 165424b4b2 Merge branch 'renovate/golang-1.x' into 'main'
chore(deps): Update golang Docker tag to v1.22

See merge request opensource/panel!38
2024-02-17 10:15:07 +00:00
机器人 d519ede9e9 chore(deps): Update golang Docker tag to v1.22 2024-02-17 10:04:52 +00:00
耗子 ebe2700cfd feat: 同步框架更新 2024-02-17 17:08:17 +08:00
耗子 bf56151db5 fix: PHP 错误日志及慢日志文件不存在的时候会报错 2024-02-17 15:58:11 +08:00
耗子 53e13d1257 feat: 暂时固定 Go 1.21 2024-02-17 15:53:04 +08:00
耗子 2732a476fd feat: 移除验证码测试 2024-02-17 15:47:31 +08:00
耗子 e8b7128f7a feat: 规范错误返回 2024-02-17 15:34:35 +08:00
耗子 ec9bd0ebb3 Merge branch 'renovate/golangci-golangci-lint-action-4.x' into 'main'
chore(deps): Update golangci/golangci-lint-action action to v4

See merge request opensource/panel!37
2024-02-14 20:50:08 +00:00
耗子 e1da619460 Merge branch 'renovate/codecov-codecov-action-4.x' into 'main'
chore(deps): Update codecov/codecov-action action to v4

See merge request opensource/panel!34
2024-02-14 20:50:01 +00:00
机器人 a90b800ede chore(deps): Update golangci/golangci-lint-action action to v4 2024-02-09 22:09:35 +00:00
机器人 d64403f49a Merge branch 'renovate/golang.org-x-crypto-0.x' into 'main'
chore(deps): Update module golang.org/x/crypto to v0.19.0

See merge request opensource/panel!36
2024-02-07 22:19:31 +00:00
机器人 527ef39a92 chore(deps): Update module golang.org/x/crypto to v0.19.0 2024-02-07 22:11:49 +00:00
机器人 82d2fab895 Merge branch 'renovate/github.com-docker-docker-25.x' into 'main'
chore(deps): Update module github.com/docker/docker to v25.0.3+incompatible

See merge request opensource/panel!35
2024-02-07 04:18:07 +00:00
机器人 bdf33163f0 chore(deps): Update module github.com/docker/docker to v25.0.3+incompatible 2024-02-07 04:10:36 +00:00
机器人 705620ab13 chore(deps): Update codecov/codecov-action action to v4 2024-02-02 22:08:46 +00:00
机器人 6e2d3eed06 Merge branch 'renovate/github.com-swaggo-swag-1.x' into 'main'
chore(deps): Update module github.com/swaggo/swag to v1.16.3

See merge request opensource/panel!33
2024-02-02 18:15:14 +00:00
机器人 053de1b848 Merge branch 'renovate/github.com-docker-docker-25.x' into 'main'
chore(deps): Update module github.com/docker/docker to v25.0.2+incompatible

See merge request opensource/panel!32
2024-02-02 18:09:52 +00:00
机器人 721d579512 chore(deps): Update module github.com/swaggo/swag to v1.16.3 2024-02-02 17:57:06 +00:00
机器人 8b8fbaa3ec chore(deps): Update module github.com/docker/docker to v25.0.2+incompatible 2024-02-02 17:55:40 +00:00
机器人 4a8a4dbcde Merge branch 'renovate/github.com-go-acme-lego-v4-4.x' into 'main'
chore(deps): Update module github.com/go-acme/lego/v4 to v4.15.0

See merge request opensource/panel!31
2024-01-28 22:21:18 +00:00
机器人 cd241f88af chore(deps): Update module github.com/go-acme/lego/v4 to v4.15.0 2024-01-28 22:09:27 +00:00
机器人 064bf7e1ed Merge branch 'renovate/github.com-gookit-validate-1.x' into 'main'
chore(deps): Update module github.com/gookit/validate to v1.5.2

See merge request opensource/panel!30
2024-01-24 16:20:39 +00:00
机器人 c1aa6e2e17 Merge branch 'renovate/github.com-docker-docker-25.x' into 'main'
chore(deps): Update module github.com/docker/docker to v25.0.1+incompatible

See merge request opensource/panel!29
2024-01-24 16:19:35 +00:00
机器人 246e53c519 chore(deps): Update module github.com/gookit/validate to v1.5.2 2024-01-24 16:09:20 +00:00
机器人 0810eee37a chore(deps): Update module github.com/docker/docker to v25.0.1+incompatible 2024-01-24 16:08:37 +00:00
耗子 c003b11ff3 chore: 更新依赖 2024-01-24 13:34:53 +08:00
耗子 d39743cdb9 chore: 调整依赖机器人配置 2024-01-24 13:18:10 +08:00
耗子 94751d474f feat(容器): 支持自定义 sock 2024-01-24 02:54:50 +08:00
耗子 63033dfa15 fix: 文档错误 2024-01-24 02:50:15 +08:00
耗子 8dc41d4222 feat: podman 初步支持 2024-01-24 02:48:56 +08:00
耗子 396fb1207e chore: update dependence 2024-01-16 16:25:51 +08:00
耗子 0ce8b15aca refactor: fallback to gin 2024-01-16 16:07:41 +08:00
耗子 55fc516049 feat(系统监控): 优化代码 2024-01-13 02:27:51 +08:00
耗子 a9c19115d4 feat(系统监控): 去除部分数据 2024-01-13 02:19:33 +08:00
耗子 9375860203 feat: 发布 v2.1.38 2024-01-13 00:19:15 +08:00
耗子 52aaadb2f9 feat: 为 TheTNB 做准备 2024-01-12 23:39:43 +08:00
耗子 c7ad1a1435 feat: 自动开启 BBR 2024-01-12 22:45:20 +08:00
耗子 c0db1cf993 feat: 添加面板每日任务 2024-01-12 22:14:10 +08:00
耗子 bad23197c1 feat: 发布 v2.1.37 2024-01-12 00:48:29 +08:00
耗子 f6dc38e61b docs: 更新 README 2024-01-12 00:48:03 +08:00
耗子 40ce8a2aad feat: 更新 OpenResty 1.25.3.1 2024-01-12 00:33:45 +08:00
耗子 999fda846d feat: 优化文案 2024-01-11 22:30:35 +08:00
耗子 958369b5ce feat: 优化文案 2024-01-11 22:29:45 +08:00
耗子 dc86e2efa3 feat: x86 下阻止在不支持 v2 指令集的 CPU 上安装 2024-01-11 22:29:09 +08:00
耗子 bb23be565a feat: 发布 v2.1.36 2024-01-10 13:57:00 +08:00
耗子 2a40cb11f7 fix: 优化部分软件 systemd 配置 2024-01-06 20:02:56 +08:00
耗子 4da6969636 fix: 安装面板生成 SSL 证书 2024-01-06 11:17:52 +08:00
耗子 bb5dd8a835 ci: optimize 2024-01-06 01:39:58 +08:00
耗子 7af5ab3c30 feat: 规范缩进 2023-12-30 18:21:37 +08:00
耗子 6a328c3a42 feat: 默认不开启 WAF 2023-12-24 15:11:04 +08:00
耗子 51ec5e137b feat: 发布 v2.1.34 2023-12-22 21:36:22 +08:00
耗子 8684727267 fix: 设置保存 2023-12-22 21:35:56 +08:00
耗子 c884dd6fd7 fix: 修复面板入口信息错误 2023-12-22 21:14:33 +08:00
耗子 35d2838036 fix: 修复面板入口信息错误 2023-12-22 21:12:48 +08:00
耗子 0acf996981 feat: 发布 v2.1.33 2023-12-22 18:05:43 +08:00
耗子 2be12f20f2 fix: 开关面板 SSL 自动重启面板 2023-12-22 18:05:29 +08:00
耗子 07b0c7a010 feat: 发布 v2.1.32 2023-12-22 18:02:45 +08:00
耗子 cf4bc94e6d fix: 修复更新脚本问题 2023-12-22 18:02:18 +08:00
耗子 d89f9553e9 chore: 调整编译参数 2023-12-22 17:32:09 +08:00
耗子 e4536360bd fix: 修复 Auth 问题 2023-12-22 17:22:38 +08:00
耗子 8bfef8030c feat: 发布 v2.1.31 2023-12-22 17:17:16 +08:00
耗子 69e972fa5e feat: 发布 v2.1.30 2023-12-22 17:06:19 +08:00
耗子 a33bd8309c feat: 优化面板证书 2023-12-22 16:31:51 +08:00
耗子 dacdfc6d45 feat: 不再自动添加 Alt-Svc 头 2023-12-22 15:49:15 +08:00
耗子 1c4e34e8fe chore: 去掉清华镜像 2023-12-20 23:43:21 +08:00
耗子 c9f650ff66 Merge branch 'renovate/major-all' into 'main'
chore(deps): Update actions/upload-artifact action to v4

See merge request opensource/panel!28
2023-12-20 14:47:13 +00:00
机器人 255f94d588 chore(deps): Update actions/upload-artifact action to v4 2023-12-14 19:03:48 +00:00
耗子 a3aaaa37ef feat: 调整 j 值算法 2023-12-14 12:22:14 +08:00
耗子 f31e3a7d8e fix: tests 2023-12-14 02:15:47 +08:00
耗子 1e462bf0e6 fix: 移除不必要的互斥锁 2023-12-14 02:12:15 +08:00
耗子 1ed79a86b2 refactor: 移动服务到根目录 2023-12-14 02:09:33 +08:00
耗子 0e4d593d6c fix: 优化 .gitignore 2023-12-14 01:45:15 +08:00
耗子 849e6602dd feat: 自动设置面板入口与显示地址信息 2023-12-14 01:42:42 +08:00
耗子 4dc16b1e03 fix: Swagger 仅在 DEBUG 模式可用 2023-12-14 01:27:34 +08:00
耗子 599f3b5cb4 fix: Swagger name 2023-12-14 01:21:39 +08:00
耗子 ccc50ed6ee refactor: 移除返回定义 2023-12-14 01:20:32 +08:00
耗子 8e84d2553f refactor: 分离服务与接口 2023-12-14 01:11:39 +08:00
耗子 d731efb082 feat: 支持 IPV6 和优化数据库位置 2023-12-14 00:51:30 +08:00
耗子 11ad8e524e feat(证书管理): 优化 CloudFlare 使用 AuthToken 2023-12-13 17:14:16 +08:00
耗子 87e975df28 fix: swagger 2023-12-13 12:26:36 +08:00
耗子 00d0cadb03 feat: 发布 v2.1.29 2023-12-10 17:21:58 +08:00
耗子 10db77ffd3 feat: 调整 QUIC 配置 2023-12-10 15:40:00 +08:00
耗子 3c82631d7b feat: 调优 WAF 配置 2023-12-10 14:06:30 +08:00
耗子 cd27c05d1c feat: 调优 OpenResty 配置 2023-12-10 13:25:10 +08:00
耗子 3a8f850a50 Merge remote-tracking branch 'origin/main' 2023-12-08 19:53:00 +08:00
耗子 06e66eaa0a Merge branch 'renovate/major-all' into 'main'
chore(deps): Update actions/setup-go action to v5

See merge request opensource/panel!27
2023-12-07 18:15:47 +00:00
耗子 2fcd3db4a4 feat: 尝试不使用包存储 2023-12-08 01:59:26 +08:00
耗子 2ec74e774b feat: 发布 v2.1.28 2023-12-08 01:38:31 +08:00
耗子 b26f75f3a3 feat: 调整 reuseport 启用位置 2023-12-08 01:38:04 +08:00
耗子 7e685dd43c feat: 发布 v2.1.27 2023-12-08 01:24:07 +08:00
耗子 3d3dcbef8d feat: 更新版本限制 2023-12-08 01:23:47 +08:00
耗子 bc2166ee66 fix: 修复 QUIC 重用端口错误 2023-12-08 01:14:05 +08:00
耗子 7cf577ff5b docs: 吹吹牛逼 2023-12-08 01:08:36 +08:00
耗子 87cdda782e feat: 发布 v2.1.26 2023-12-08 00:11:52 +08:00
耗子 24106288cb feat: QUIC 与 IPV6 实验性支持 2023-12-08 00:11:11 +08:00
机器人 3f258a5f81 chore(deps): Update actions/setup-go action to v5 2023-12-06 16:03:41 +00:00
耗子 ce8addfecd feat: 发布 v2.1.25 2023-12-02 14:24:59 +08:00
耗子 d969360b54 fix: 优化 OpenResty 编译脚本 2023-12-02 14:24:37 +08:00
耗子 1f43a12f81 fix: patch OpenSSL for OpenResty 2023-12-01 22:22:18 +08:00
耗子 930b86a671 feat: 更新 OpenSSL 2023-12-01 21:58:55 +08:00
耗子 790e56003e feat: 发布 v2.1.24 2023-12-01 16:13:19 +08:00
耗子 c38736abb6 fix: 网站配置读取防跨站可能出错 2023-11-30 22:22:04 +08:00
耗子 befad1d3ac fix: 创建网站时创建 PostgreSQL 未设置用户权限 2023-11-30 21:30:19 +08:00
耗子 53d8c2ff6d feat: 发布 v2.1.23 2023-11-29 21:23:46 +08:00
耗子 d07314b5be feat: 优化 supervisor 添加 2023-11-29 21:12:43 +08:00
耗子 f04cafbcee fix: lint 2023-11-29 02:38:06 +08:00
耗子 86199b9eec feat: 替换为自建 Git 2023-11-29 02:29:22 +08:00
耗子 e4e3ad5821 feat: 日志倒序输出 2023-11-29 02:26:23 +08:00
耗子 a80ddfaebc feat: 文件管理接口 2023-11-24 02:49:40 +08:00
耗子 026e1319fe feat: 发布 v2.1.22 2023-11-23 23:39:04 +08:00
耗子 b532064cf0 feat: PHP 83 2023-11-23 23:38:44 +08:00
耗子 f900ada2c6 feat: 优化 supervisor 配置保存 2023-11-23 22:37:21 +08:00
耗子 468b20348a fix: 优化 Rsync 卸载 2023-11-23 22:31:39 +08:00
耗子 862686e4e2 fix: 修复 Rsync secret 读取 2023-11-23 21:48:50 +08:00
耗子 5a3a1e694b feat: Rsync 插件添加配置接口 2023-11-23 21:42:50 +08:00
耗子 a368117e00 feat: Rsync 插件 2023-11-23 21:38:48 +08:00
耗子 596cd68728 feat: 更新前检查临时文件 2023-11-23 16:38:41 +08:00
耗子 d55bb5ffe1 feat: Rsync 插件 2023-11-23 02:56:20 +08:00
耗子 94ff9e2dbc feat: 更新 s3fs 和 supervisor 的升级脚本 2023-11-22 15:44:40 +08:00
耗子 f4cb1a850a fix: 同步框架更新 2023-11-22 15:36:41 +08:00
耗子 7d1acae966 feat: 发布 v2.1.20 2023-11-21 20:37:28 +08:00
耗子 18b524bddc feat: 优化 MySQL 安装 2023-11-21 20:36:54 +08:00
耗子 70596a7906 chore: update framework 2023-11-21 11:55:51 +08:00
耗子 fbf0b7acce fix: mysql_upgrade 只在 57 版本运行 2023-11-21 11:19:46 +08:00
耗子 12ad6e8879 feat: 发布 v2.1.19 2023-11-21 11:13:46 +08:00
耗子 b4ea470099 feat: 优化部分软件多线程编译参数 2023-11-21 11:13:14 +08:00
耗子 bb9f629b8e feat: 发布 v2.1.18 2023-11-21 07:51:20 +08:00
耗子 fb099b1124 fix: 漏了一个 2023-11-21 03:30:55 +08:00
耗子 8584f0809e fix: OpenSSL checksums 错误 2023-11-21 03:29:15 +08:00
耗子 8ae3751f0b fix: OpenSSL checksums 错误 2023-11-21 03:27:10 +08:00
331 changed files with 24876 additions and 11497 deletions
-32
View File
@@ -1,32 +0,0 @@
root = "."
tmp_dir = "storage/temp"
[build]
bin = "./storage/temp/main.exe --env=panel.conf"
cmd = "go build -o ./storage/temp/main.exe ."
delay = 1000
exclude_dir = ["storage", "database"]
exclude_file = []
exclude_regex = []
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false
+2 -2
View File
@@ -1,4 +1,4 @@
# These are supported funding model platforms
open_collective: haozi-panel
custom: ['https://afdian.net/a/haozi-team']
open_collective: tnb
custom: [ 'https://afdian.net/a/TheTNB' ]
+4 -4
View File
@@ -13,10 +13,10 @@ jobs:
fail-fast: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
cache: true
go-version: 'stable'
go-version: '1.22'
- name: Install dependencies
run: go mod tidy
- name: Build ${{ matrix.goarch }}
@@ -24,9 +24,9 @@ jobs:
CGO_ENABLED: 0
GOOS: linux
GOARCH: ${{ matrix.goarch }}
run: go build -ldflags '-s -w --extldflags "-static -fpic"' -o panel-${{ matrix.goarch }}
run: go build -ldflags '-s -w --extldflags "-static"' -tags='nomsgpack' -o panel-${{ matrix.goarch }}
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: panel-${{ matrix.goarch }}
path: panel-${{ matrix.goarch }}
+3 -3
View File
@@ -9,15 +9,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
go-version: '1.22'
- 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@v3
uses: codecov/codecov-action@v4
with:
file: ./coverage.out
token: ${{ secrets.CODECOV }}
+5 -6
View File
@@ -14,18 +14,17 @@ jobs:
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: 'stable'
go-version: '1.22'
- name: Fetch Frontend
run: |
sudo apt-get install -y curl jq unzip zip
curl -sSL https://api.github.com/repos/haozi-team/panel-frontend/releases/latest | jq -r ".assets[] | select(.name | contains(\"dist\")) | .browser_download_url" | xargs curl -L -o frontend.zip
rm -rf public
curl -sSL https://api.github.com/repos/TheTNB/panel-frontend/releases/latest | jq -r ".assets[] | select(.name | contains(\"dist\")) | .browser_download_url" | xargs curl -L -o frontend.zip
unzip frontend.zip
mv dist public
mv dist/* embed/frontend/
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
我们认为您的建议非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献!
We think your suggestion is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution!
![aoligei](https://github.com/haozi-team/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0)
![aoligei](https://github.com/TheTNB/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0)
- name: ☢️ Bug
if: github.event.label.name == '☢️ Bug'
uses: actions-cool/issues-helper@v3
@@ -41,4 +41,4 @@ jobs:
我们认为您的反馈非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献!
We think your feedback is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution!
![aoligei](https://github.com/haozi-team/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0)
![aoligei](https://github.com/TheTNB/panel/assets/115467771/fb04debf-3f4c-4fac-a0b8-c3455f8e57a0)
@@ -1,25 +0,0 @@
name: Issue Close Question
on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: read
jobs:
issue-close-require:
permissions:
issues: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: needs more info
uses: actions-cool/issues-helper@v3
with:
actions: 'close-issues'
labels: 'question'
inactive-day: 3
body: |
由于该 Issue 3天未收到回应,现已被自动关闭,若有任何问题,可评论回复。
This issue has been closed automatically because it has not had recent activity for 3 days. If you have any questions, please comment here.
+3 -5
View File
@@ -12,15 +12,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
go-version: '1.22'
cache: false
- name: Lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v6
with:
skip-cache: true
skip-pkg-cache: true
skip-build-cache: true
version: latest
args: --timeout=30m ./...
+3 -4
View File
@@ -9,16 +9,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
go-version: '1.22'
- name: Install dependencies
run: sudo apt-get install -y curl jq
- name: Set up environment
run: |
cp panel-example.conf .env
echo "DB_FILE=$(pwd)/database/panel.db" >> .env
echo "DB_FILE=$(pwd)/storage/panel.db" >> .env
go run . artisan key:generate
go run . artisan migrate
- name: Run tests
run: go test ./...
-5
View File
@@ -67,8 +67,3 @@ out/
*.sublime*
__debug_bin
.project
# 前端工具链 #
.sass-cache/*
node_modules/
/public
+11 -17
View File
@@ -1,4 +1,4 @@
image: golang:bookworm
image: golang:1.22-bookworm
# 在每个任务执行前运行
before_script:
@@ -14,12 +14,6 @@ before_script:
paths:
- .go/pkg/mod/
# 全局变量
variables:
OUTPUT_NAME: "panel"
GO111MODULE: "on"
GOPROXY: "https://goproxy.cn,direct"
stages:
- prepare
- build
@@ -44,9 +38,8 @@ unit_test:
- apt-get update
- apt-get install -y curl jq
- cp panel-example.conf .env
- echo "DB_FILE=$(pwd)/database/panel.db" >> .env
- echo "DB_FILE=$(pwd)/storage/panel.db" >> .env
- go run . artisan key:generate
- go run . artisan migrate
- go test -v -coverprofile=coverage.txt -covermode=atomic ./...
build:
@@ -54,28 +47,27 @@ build:
extends: .go_cache
script:
- go mod download
- CGO_ENABLED=0 go build -ldflags '-s -w --extldflags "-static -fpic"' -o $OUTPUT_NAME
- CGO_ENABLED=0 go build -ldflags '-s -w --extldflags "-static"' -tags='nomsgpack' -o panel
artifacts:
name: "$OUTPUT_NAME"
name: "panel"
paths:
- $OUTPUT_NAME
- panel
expire_in: 3 days
fetch:
stage: build
image: alpine:latest
before_script:
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
- apk add --no-cache curl jq unzip zip
script:
- curl -sSL "https://jihulab.com/api/v4/projects/haozi-team%2Fpanel-frontend/releases" | jq -r '.[0].assets.links[] | select(.name | contains("dist")) | .direct_asset_url' | xargs curl -L -o frontend.zip
- rm -rf public
- curl -sSL "https://git.haozi.net/api/v4/projects/opensource%2Fpanel-frontend/releases" | jq -r '.[0].assets.links[] | select(.name | contains("dist")) | .direct_asset_url' | xargs curl -L -o frontend.zip
- unzip frontend.zip
- mv dist public
- mv dist/* embed/frontend/
artifacts:
name: "frontend"
paths:
- public
- embed/frontend
expire_in: 3 days
release:
@@ -93,4 +85,6 @@ release:
# generate a changelog.
GIT_DEPTH: 0
script:
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
- apk add --no-cache upx
- goreleaser release --clean
+23 -13
View File
@@ -1,7 +1,7 @@
project_name: panel
builds:
-
id: panel
- id: panel
binary: panel
env:
- CGO_ENABLED=0
@@ -14,37 +14,47 @@ builds:
goamd64:
- v2
ldflags:
- -s -w --extldflags "-static -fpic"
- -s -w --extldflags "-static"
tags:
- nomsgpack
upx:
- enabled: true
# Filter by build ID.
ids: [ panel ]
# Compress argument.
# Valid options are from '1' (faster) to '9' (better), and 'best'.
compress: best
# Whether to try LZMA (slower).
lzma: true
# Whether to try all methods and filters (slow).
brute: false
archives:
-
id: panel-archive
- id: panel-archive
builds:
- panel
format: zip
wrap_in_directory: false
strip_parent_binary_folder: true
strip_binary_directory: true
files:
- LICENSE
- docs/*
- public/*
- storage/*
- database/*
- lang/*
- scripts/*
- panel-example.conf
gitlab_urls:
api: https://jihulab.com/api/v4/
download: https://jihulab.com
api: https://git.haozi.net/api/v4/
download: https://git.haozi.net
# set to true if you use a self-signed certificate
skip_tls_verify: false
# set to true if you want to upload to the Package Registry rather than attachments
# Only works with GitLab 13.5+
#
# Since: v1.3
use_package_registry: true
# Set this if you set GITLAB_TOKEN to the value of CI_JOB_TOKEN.
#
# Since: v1.11
-1
View File
@@ -1 +0,0 @@
replace carbon.DateTime string
+2 -2
View File
@@ -1,6 +1,6 @@
## 行为准则
耗子Linux面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
耗子面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
- 参与者将容忍反对意见。
- 参与者必须确保他们的语言和行为没有人身攻击和贬低个人言论。
@@ -9,7 +9,7 @@
## Code of Conduct
The HaoZi Linux Panel complies with the industry's common code of conduct. Any breach of the Code of Conduct can be reported to us:
The Rat Panel complies with the industry's common code of conduct. Any breach of the Code of Conduct can be reported to us:
- Participants will be tolerant of opposing views.
- Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
+46 -47
View File
@@ -2,28 +2,33 @@
[简体中文] | [<a href="README_EN.md">English</a>]
</p>
<h1 align="center">耗子 Linux 面板</h1>
<h1 align="center" style="font-size: 40px">耗子面板</h1>
<p align="center">
<a href="https://github.com/haozi-team/panel/releases"><img src="https://img.shields.io/github/release/haozi-team/panel.svg"></a>
<a href="https://github.com/haozi-team/panel/actions"><img src="https://github.com/haozi-team/panel/actions/workflows/test.yml/badge.svg"></a>
<a href="https://goreportcard.com/report/github.com/haozi-team/panel"><img src="https://goreportcard.com/badge/github.com/haozi-team/panel"></a>
<a href="https://codecov.io/gh/haozi-team/panel"><img src="https://codecov.io/gh/haozi-team/panel/branch/main/graph/badge.svg?token=XFT5NGNSRG"></a>
<a href="https://img.shields.io/github/license/haozi-team/panel"><img src="https://img.shields.io/github/license/haozi-team/panel"></a>
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2Fhaozi-team%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fhaozi-team%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
<a href="https://trendshift.io/repositories/10950" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10950" alt="TheTNB%2Fpanel | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>
耗子 Linux 面板是使用 Golang + Vue 开发的轻量 Linux 服务器运维管理面板,以 GNU Affero General Public License v3.0 开源。
<p align="center">
<a href="https://github.com/TheTNB/panel/releases"><img src="https://img.shields.io/github/release/TheTNB/panel.svg"></a>
<a href="https://github.com/TheTNB/panel/actions"><img src="https://github.com/TheTNB/panel/actions/workflows/test.yml/badge.svg"></a>
<a href="https://goreportcard.com/report/github.com/TheTNB/panel"><img src="https://goreportcard.com/badge/github.com/TheTNB/panel"></a>
<a href="https://img.shields.io/github/license/TheTNB/panel"><img src="https://img.shields.io/github/license/TheTNB/panel"></a>
<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>
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[pd.qq.com/s/fyol46wfy](https://pd.qq.com/s/fyol46wfy)
耗子面板是使用 Golang + Vue 开发的开源轻量 Linux 服务器运维管理面板。
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.net/a/TheTNB)
## 优势
1. **极低占用:** 在 Debian 12 下部署面板 + LNMP 环境,内存占用不到 500 MB,遥遥领先于使用容器化的其他面板。
2. **高效运维:** 面板 UI 界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置
3. **离线运行:** 面板运行可不依赖任何外部服务,你甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响
4. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 1 年无事故
5. **开源开放:** 面板开源,你可以自由修改、审计面板源码,安全性有保障
2. **低破坏性:** 面板的设计理念是尽可能减少对系统的额外修改,在同类面板中,我们对系统的修改最少
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障
4. **高效运维:** 面板 UI 界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置
5. **离线运行:** 面板运行可不依赖任何外部服务,你甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 1 年以上无事故。
7. **开源开放:** 面板开源,你可以自由修改、审计面板源码,安全性有保障。
## UI 截图
@@ -31,13 +36,11 @@
## 运行环境
耗子Linux面板仅支持 `amd64` | `arm64` 架构下的主流系统的最新版本,不支持 `Ubuntu`,因为其发版太过频繁,难以维护。
耗子面板仅支持 `amd64` | `arm64` 架构下的主流系统的最新版本,不支持 `Ubuntu`,因为其发版太过频繁,难以维护。
低配机器建议使用 `Debian`,资源占用较 `RHEL` 系更低。其他机器建议使用 `AlmaLinux` | `RockyLinux`,维护周期更长也更稳定。
不在下表中的其他系统OpenCloudOS 8、Anolis 8、CentOS Stream 8/9、Debian 11等),可自行尝试安装,但不保证能够正常运行,且不提供无偿技术支持(理论上不会有大问题)。
CentOS Stream 可使用迁移脚本迁移至支持的系统: [CentOS 8/9 迁移脚本](https://github.com/haozi-team/byecentos)
不在下表中的其他 RHEL 9.x 系统,可自行尝试安装,但不保证能够正常运行,且不提供技术支持(理论上不会有大问题)。
| 系统 | 版本 |
|------------|----|
@@ -51,12 +54,12 @@ CentOS Stream 可使用迁移脚本迁移至支持的系统: [CentOS 8/9 迁移
## 安装面板
> **Warning**
> 安装面板前,你需要了解LNMP环境的基本知识,以及如何处理常见的LNMP环境问题,不建议0基础的用户安装和使用耗子Linux面板。
> 安装面板前,你需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,不建议 0 基础的用户安装和使用耗子面板。
如果你决定继续,请以`root`用户登录服务器,执行以下命令安装面板:
```shell
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
```
## 卸载面板
@@ -66,7 +69,7 @@ HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -s
如果你无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
```shell
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
```
卸载面板前请务必备份好所有数据,提前卸载面板全部插件。卸载后数据将**无法恢复**!
@@ -79,57 +82,53 @@ HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -s
panel
```
在 [Wiki](https://github.com/haozi-team/panel/wiki) 中查看更多使用方法和技巧。
在 [Wiki](https://github.com/TheTNB/panel/wiki) 中查看更多使用方法和技巧。
## 问题反馈
使用类问题,可在 [WePublish 社区论坛](https://wepublish.cn/forums) 提问或QQ群`@坤坤`寻求 AI 帮助,亦可在群里寻求付费支持。
使用类问题,可在 [Moe Tom](https://tom.moe) 提问或寻求 AI 帮助,亦可在群里寻求付费支持。
面板自身问题,可在 GitHub 的`Issues`页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。
## 贡献代码
### 寻找/创建 Issue
您可以在 [Issue 列表](https://github.com/haozi-team/panel/issues) 中寻找或创建一个 Issue,留言表达想要处理该 Issue 的意愿,得到维护者的确认后,即可开始处理。
### 创建 PR
- 在开发过程中,如果遇到问题可以随时在 Issue 中详尽描述该问题,以进一步沟通,但在此之前请确保自己已通过 Google 等方式尽可能的尝试解决问题;
- PR 须提交至我们的极狐 GitLab 仓库[https://jihulab.com/haozi-team/panel](https://jihulab.com/haozi-team/panel),勿在 GitHub 上提交;
- 当 PR 开发完毕后,请为其添加 `🚀 Review Ready` 标签,维护者将及时进行评审;
- 我们非常欢迎您的贡献,将在下次发版时将您添加到首页贡献者中;❤️
## 赞助商
如果耗子面板对你有帮助,欢迎[赞助我们](https://afdian.net/a/TheTNB),感谢以下支持者/赞助商的支持:
**同时接受云资源赞助,可通过QQ群咨询联系**
### 服务器
- [微晓朵](https://www.weixiaoduo.com/)
### CDN
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
- [盾云CDN](http://cdn.ddunyun.com/)
### DevOps
- [极狐 GitLab](https://www.jihulab.com/)
**接受云资源和资金赞助,可通过QQ群咨询联系**
<p align="center">
<a target="_blank" href="https://afdian.net/a/TheTNB">
<img alt="sponsors" src="https://github.com/TheTNB/sponsor/blob/main/sponsors.svg?raw=true"/>
</a>
</p>
## 贡献者
这个项目的存在要归功于所有做出贡献的人,参与贡献请先查看贡献代码部分。
<a href="https://github.com/DevHaoZi" target="_blank"><img src="https://avatars.githubusercontent.com/u/115467771?v=4" width="48" height="48"></a>
<a href="https://github.com/TheTNB/panel/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=TheTNB/panel"/>
</a>
## Star 历史
<a href="https://star-history.com/#haozi-team/panel&Date">
<a href="https://star-history.com/#TheTNB/panel&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
<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>
## 免责声明
严禁使用耗子 Linux 面板从事任何非法活动,非法站点请勿向我们请求任何形式的技术支持,如果在技术支持过程中发现非法内容,我们将立即停止技术支持并留存相关证据。
严禁使用耗子面板从事任何非法活动,非法站点请勿向我们请求任何形式的技术支持,如果在技术支持过程中发现非法内容,我们将立即停止技术支持并留存相关证据。
+46 -47
View File
@@ -2,28 +2,33 @@
[<a href="README.md">简体中文</a>] | [English]
</p>
<h1 align="center">HaoZi Linux Panel</h1>
<h1 align="center" style="font-size: 40px">Rat Panel</h1>
<p align="center">
<a href="https://github.com/haozi-team/panel/releases"><img src="https://img.shields.io/github/release/haozi-team/panel.svg"></a>
<a href="https://github.com/haozi-team/panel/actions"><img src="https://github.com/haozi-team/panel/actions/workflows/test.yml/badge.svg"></a>
<a href="https://goreportcard.com/report/github.com/haozi-team/panel"><img src="https://goreportcard.com/badge/github.com/haozi-team/panel"></a>
<a href="https://codecov.io/gh/haozi-team/panel"><img src="https://codecov.io/gh/haozi-team/panel/branch/main/graph/badge.svg?token=XFT5NGNSRG"></a>
<a href="https://img.shields.io/github/license/haozi-team/panel"><img src="https://img.shields.io/github/license/haozi-team/panel"></a>
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2Fhaozi-team%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fhaozi-team%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
<a href="https://trendshift.io/repositories/10950" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10950" alt="TheTNB%2Fpanel | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>
The HaoZi Linux Panel is a lightweight Linux server operation and maintenance management panel developed using Golang and Vue. It is open source under the GNU Affero General Public License v3.0 protocol.
<p align="center">
<a href="https://github.com/TheTNB/panel/releases"><img src="https://img.shields.io/github/release/TheTNB/panel.svg"></a>
<a href="https://github.com/TheTNB/panel/actions"><img src="https://github.com/TheTNB/panel/actions/workflows/test.yml/badge.svg"></a>
<a href="https://goreportcard.com/report/github.com/TheTNB/panel"><img src="https://goreportcard.com/badge/github.com/TheTNB/panel"></a>
<a href="https://img.shields.io/github/license/TheTNB/panel"><img src="https://img.shields.io/github/license/TheTNB/panel"></a>
<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>
Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ channel: [pd.qq.com/s/fyol46wfy](https://pd.qq.com/s/fyol46wfy)
The Rat Panel is an open source lightweight Linux server operation and maintenance management panel developed using Golang and Vue.
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)
## Advantages
1. **Extremely low usage:** Deploying the panel + LNMP environment under Debian 12, the memory usage is less than 500 MB, far ahead of other panels using containerization.
2. **Efficient operation and maintenance:** The panel UI interface is simple and easy to operate, and you can quickly deploy various environments and adjust application settings without complicated configuration.
3. **Offline operation:** The panel can run without relying on any external services. You can even stop the panel process after deployment is complete, and it will not affect the deployed services.
4. **Tested by time:** We have been using it in production environment since 2022, and it has been running stably for 1 year without any accidents.
5. **Open source and open:** The panel is open source, you can freely modify and audit the panel source code, and the security is guaranteed.
2. **Low destructiveness:** The design concept of the panel is to minimize the additional modifications to the system. Among similar panels, we have made the least modifications to the system.
3. **Follow the times:** All components of the panel are at the forefront of the times, updated quickly, powerful functions, and guaranteed security.
4. **Efficient operation and maintenance:** The panel UI interface is simple and easy to operate, and you can quickly deploy various environments and adjust application settings without complicated configuration.
5. **Offline operation:** The panel can run without relying on any external services. You can even stop the panel process after deployment is complete, and it will not affect the deployed services.
6. **Tested by time:** We have been using it in production environment since 2022, and it has been running stably for more than 1 year without any accidents.
7. **Open source and open:** The panel is open source, you can freely modify and audit the panel source code, and the security is guaranteed.
## UI Screenshots
@@ -31,13 +36,11 @@ Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ
## Operating Environment
HaoZi Linux Panel only supports the latest version of mainstream systems under the `amd64` | `arm64` architecture. It does not support `Ubuntu` because its releases are too frequent and difficult to maintain.
Rat Panel only supports the latest version of mainstream systems under the `amd64` | `arm64` architecture. It does not support `Ubuntu` because its releases are too frequent and difficult to maintain.
Recommended to use `Debian` for low-configuration machines, as its resource usage is lower than that of the `RHEL` system. For other machines, recommended to use `RockyLinux` | `AlmaLinux`, which has a longer maintenance cycle and is more stable.
For other systems not in the table below (OpenCloudOS 8, Anolis 8, CentOS Stream 8/9, Debian 11, etc.), you can try to install it yourself, but normal operation is not guaranteed, and free technical support is not provided (theoretically there will be no major question).
CentOS Stream can be migrated to a supported system using the migration script: [CentOS 8/9 Migration Script](https://github.com/haozi-team/byecentos)
For other RHEL 9.x systems not in the table, you can try to install it yourself, but normal operation is not guaranteed, and technical support is not provided (theoretically there will be no major question).
| OS | Version |
|------------|---------|
@@ -51,12 +54,12 @@ As system versions are constantly updated, we may also terminate support for som
## 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. It is not recommended for users with zero basic knowledge to install and use HaoZi Linux Panel.
> Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. It is 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:
```shell
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
```
## Uninstall Panel
@@ -66,7 +69,7 @@ Recommended to back up data and reinstall the system first, so that the system c
If you are unable to reinstall the system, log in to the server as the `root` user and execute the following command to uninstall the panel:
```shell
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
HAOZI_DL_URL="https://dl.cdn.haozi.net/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
```
Before uninstalling the panel, please be sure to back up all data and uninstall all panel plugins in advance. The data will **not be recoverable** after uninstallation!
@@ -79,57 +82,53 @@ Use `panel` command for daily maintenance:
panel
```
See more usage methods and tips in [Wiki](https://github.com/haozi-team/panel/wiki).
See more usage methods and tips in [Wiki](https://github.com/TheTNB/panel/wiki).
## Feedback
For usage issues, you can ask questions in the [WePublish Community Forum](https://wepublish.cn/forums) or seek AI help in the QQ group `@坤坤`. You can also seek paid support in the group.
For usage issues, you can ask questions in the [Moe Tom](https://tom.moe) or seek AI help. You can also seek paid support in the group.
For issues with the panel itself, you can submit feedback on the `Issues` page of GitHub. Please note [the wisdom of asking questions](http://www.catb.org/~esr/faqs/smart-questions.html).
## Contribute code
### Find/Create Issue
You can find or create an Issue in [Issue List](https://github.com/haozi-team/panel/issues), leave a message to express your willingness to deal with the Issue, and get confirmation from the maintainer. Start processing.
### Create PR
- During the development process, if you encounter a problem, you can describe the problem in detail in the Issue at any time for further communication, but before doing so, please make sure that you have tried your best to solve the problem through Google and other methods;
- PRs must be submitted to our JiHu GitLab repository [https://jihulab.com/haozi-team/panel](https://jihulab.com/haozi-team/panel). Do not submit on GitHub;
- When the PR is completed, please add the `🚀 Review Ready` tag to it, and the maintainer will review it in time;
- We very much welcome your contributions and will add you to the homepage contributors in the next release; ❤️
## Sponsor
If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollective.com/tnb), thanks to the following supporters/sponsors:
**Accept cloud resources and financial sponsorship, you can contact us through QQ group**
### Server
- [微晓朵](https://www.weixiaoduo.com/)
### CDN
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
- [盾云CDN](http://cdn.ddunyun.com/)
### DevOps
- [JiHu GitLab](https://www.jihulab.com/)
**Accept cloud resources and financial sponsorship, you can contact us through QQ group**
<p align="center">
<a target="_blank" href="https://afdian.net/a/TheTNB">
<img alt="sponsors" src="https://github.com/TheTNB/sponsor/blob/main/sponsors.svg?raw=true"/>
</a>
</p>
## Contributor
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/DevHaoZi" target="_blank"><img src="https://avatars.githubusercontent.com/u/115467771?v=4" width="48" height="48"></a>
<a href="https://github.com/TheTNB/panel/graphs/contributors">
<img alt="contributors" src="https://contrib.rocks/image?repo=TheTNB/panel"/>
</a>
## Star History
<a href="https://star-history.com/#haozi-team/panel&Date">
<a href="https://star-history.com/#TheTNB/panel&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
<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>
## Disclaimer
It is strictly prohibited to use the HaoZi Linux Panel to engage in any illegal activities. Please do not request any form of technical support from us for illegal sites. If illegal content is discovered during the technical support process, we will immediately stop technical support and retain relevant evidence.
It is strictly prohibited to use the Rat Panel to engage in any illegal activities. Please do not request any form of technical support from us for illegal sites. If illegal content is discovered during the technical support process, we will immediately stop technical support and retain relevant evidence.
+4 -4
View File
@@ -1,8 +1,8 @@
## 安全说明
安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子Linux面板,至今无一例安全事故。
安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子面板,至今无一例安全事故。
耗子Linux面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证**。
耗子面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证**。
如果您在使用面板的过程中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
@@ -13,9 +13,9 @@
## Security Policy
Security is one of our biggest concerns. We have widely used HaoZi Linux Panel in production environments for many different applications, and there has been no safety incident so far.
Security is one of our biggest concerns. We have widely used Rat Panel in production environments for many different applications, and there has been no safety incident so far.
HaoZi Linux Panel adopts various solutions in the industry to ensure the security of the panel as much as possible, but we cannot guarantee the absolute security of the panel, **therefore we do not make any guarantees on the security of the panel**.
Rat Panel adopts various solutions in the industry to ensure the security of the panel as much as possible, but we cannot guarantee the absolute security of the panel, **therefore we do not make any guarantees on the security of the panel**.
If you find any security issues while using the panel, please do not submit an Issue. You can contact us directly through the following methods:
+21 -17
View File
@@ -1,18 +1,20 @@
package commands
import (
"crypto/x509"
"encoding/pem"
"context"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"panel/app/models"
"panel/app/services"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal/services"
panelcert "github.com/TheTNB/panel/pkg/cert"
"github.com/TheTNB/panel/pkg/types"
)
// CertRenew 证书续签
type CertRenew struct {
}
@@ -23,7 +25,8 @@ func (receiver *CertRenew) Signature() string {
// Description The console command description.
func (receiver *CertRenew) Description() string {
return "[面板] 证书续签"
ctx := context.Background()
return facades.Lang(ctx).Get("commands.panel:cert-renew.description")
}
// Extend The console command extend.
@@ -34,7 +37,11 @@ func (receiver *CertRenew) Extend() command.Extend {
}
// Handle Execute the console command.
func (receiver *CertRenew) Handle(ctx console.Context) error {
func (receiver *CertRenew) Handle(console.Context) error {
if types.Status != types.StatusNormal {
return nil
}
var certs []models.Cert
err := facades.Orm().Query().With("Website").With("User").With("DNS").Find(&certs)
if err != nil {
@@ -46,18 +53,15 @@ func (receiver *CertRenew) Handle(ctx console.Context) error {
continue
}
block, _ := pem.Decode([]byte(cert.Cert))
if block != nil {
data, err := x509.ParseCertificate(block.Bytes)
if err != nil {
continue
}
decode, err := panelcert.ParseCert(cert.Cert)
if err != nil {
continue
}
// 结束时间大于 7 天的证书不续签
endTime := carbon.FromStdTime(data.NotAfter)
if endTime.Gt(carbon.Now().AddDays(7)) {
continue
}
// 结束时间大于 7 天的证书不续签
endTime := carbon.FromStdTime(decode.NotAfter)
if endTime.Gt(carbon.Now().AddDays(7)) {
continue
}
certService := services.NewCertImpl()
+39 -24
View File
@@ -1,20 +1,22 @@
package commands
import (
"strconv"
"context"
"github.com/gookit/color"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/goravel/framework/support/color"
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
)
// Monitoring 系统监控
type Monitoring struct {
}
@@ -25,7 +27,8 @@ func (receiver *Monitoring) Signature() string {
// Description The console command description.
func (receiver *Monitoring) Description() string {
return "[面板] 系统监控"
ctx := context.Background()
return facades.Lang(ctx).Get("commands.panel:monitoring.description")
}
// Extend The console command extend.
@@ -36,40 +39,52 @@ func (receiver *Monitoring) Extend() command.Extend {
}
// Handle Execute the console command.
func (receiver *Monitoring) Handle(ctx console.Context) error {
var setting models.Setting
monitor := services.NewSettingImpl().Get(models.SettingKeyMonitor)
func (receiver *Monitoring) Handle(console.Context) error {
if types.Status != types.StatusNormal {
return nil
}
// 将等待中的任务分发
// TODO 有bug,需要设计一个锁机制防止重复分发
task := services.NewTaskImpl()
_ = task.DispatchWaiting()
setting := services.NewSettingImpl()
monitor := setting.Get(models.SettingKeyMonitor)
if !cast.ToBool(monitor) {
return nil
}
info := tools.GetMonitoringInfo()
translate := facades.Lang(context.Background())
// 去除部分数据以减少数据库存储
info.Disk = nil
info.Cpus = nil
if types.Status != types.StatusNormal {
return nil
}
err := facades.Orm().Query().Create(&models.Monitor{
Info: info,
})
if err != nil {
facades.Log().Infof("[面板] 系统监控保存失败: %s", err.Error())
color.Redf("[面板] 系统监控保存失败: %s", err.Error())
facades.Log().Tags("面板", "系统监控").With(map[string]any{
"error": err.Error(),
}).Infof("保存失败")
color.Red().Printfln(translate.Get("commands.panel:monitoring.fail")+": %s", err.Error())
return nil
}
// 删除过期数据
err = facades.Orm().Query().Where("key", "monitor_days").First(&setting)
if err != nil {
days := cast.ToInt(setting.Get(models.SettingKeyMonitorDays))
if days <= 0 || types.Status != types.StatusNormal {
return nil
}
if setting.Value == "0" || len(setting.Value) == 0 {
return nil
}
days, err := strconv.Atoi(setting.Value)
if err != nil {
return nil
}
_, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{})
if err != nil {
facades.Log().Infof("[面板] 系统监控删除过期数据失败: %s", err.Error())
if _, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{}); err != nil {
facades.Log().Tags("面板", "系统监控").With(map[string]any{
"error": err.Error(),
}).Infof("删除过期数据失败")
return nil
}
+333 -175
View File
@@ -1,24 +1,32 @@
package commands
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"github.com/gookit/color"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/goravel/framework/support/color"
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
requests "github.com/TheTNB/panel/app/http/requests/website"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal/services"
"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"
)
// Panel 面板命令行
type Panel struct {
}
@@ -29,7 +37,8 @@ func (receiver *Panel) Signature() string {
// Description The console command description.
func (receiver *Panel) Description() string {
return "[面板] 命令行"
ctx := context.Background()
return facades.Lang(ctx).Get("commands.panel.description")
}
// Extend The console command extend.
@@ -46,126 +55,149 @@ func (receiver *Panel) Handle(ctx console.Context) error {
arg2 := ctx.Argument(2)
arg3 := ctx.Argument(3)
arg4 := ctx.Argument(4)
arg5 := ctx.Argument(5)
translate := facades.Lang(context.Background())
switch action {
case "init":
var check models.User
err := facades.Orm().Query().FirstOrFail(&check)
if err == nil {
color.Redln("面板已初始化")
color.Red().Printfln(translate.Get("commands.panel.init.exist"))
return nil
}
settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子Linux面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyVersion, Value: facades.Config().GetString("panel.version")}}
settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyVersion, Value: facades.Config().GetString("panel.version")}}
err = facades.Orm().Query().Create(&settings)
if err != nil {
color.Redln("初始化失败")
color.Red().Printfln(translate.Get("commands.panel.init.fail"))
return nil
}
hash, err := facades.Hash().Make(tools.RandomString(32))
hash, err := facades.Hash().Make(str.RandomString(32))
if err != nil {
color.Redln("初始化失败")
color.Red().Printfln(translate.Get("commands.panel.init.fail"))
return nil
}
user := services.NewUserImpl()
_, err = user.Create("admin", hash)
if err != nil {
color.Redln("创建管理员失败")
color.Red().Printfln(translate.Get("commands.panel.init.adminFail"))
return nil
}
color.Greenln("初始化成功")
color.Green().Printfln(translate.Get("commands.panel.init.success"))
case "update":
var task models.Task
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
if err == nil {
color.Redln("当前有任务正在执行,禁止更新")
if err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task); err == nil {
color.Red().Printfln(translate.Get("commands.panel.update.taskCheck"))
return nil
}
if _, err := facades.Orm().Query().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
types.Status = types.StatusFailed
color.Red().Printfln(translate.Get("commands.panel.update.dbFail"))
return nil
}
panel, err := tools.GetLatestPanelVersion()
if err != nil {
color.Redln("获取最新版本失败")
color.Red().Printfln(translate.Get("commands.panel.update.versionFail"))
return err
}
err = tools.UpdatePanel(panel)
if err != nil {
color.Redln("更新失败: " + err.Error())
// 停止面板服务,因为在shell中运行的和systemd的不同
_ = systemctl.Stop("panel")
types.Status = types.StatusUpgrade
if err = tools.UpdatePanel(panel); err != nil {
types.Status = types.StatusFailed
color.Red().Printfln(translate.Get("commands.panel.update.fail") + ": " + err.Error())
return nil
}
color.Greenln("更新成功")
types.Status = types.StatusNormal
color.Green().Printfln(translate.Get("commands.panel.update.success"))
tools.RestartPanel()
case "getInfo":
var user models.User
err := facades.Orm().Query().Where("id", 1).FirstOrFail(&user)
if err != nil {
color.Redln("获取管理员信息失败")
color.Red().Printfln(translate.Get("commands.panel.getInfo.adminGetFail"))
return nil
}
password := tools.RandomString(16)
password := str.RandomString(16)
hash, err := facades.Hash().Make(password)
if err != nil {
color.Redln("生成密码失败")
color.Red().Printfln(translate.Get("commands.panel.getInfo.passwordGenerationFail"))
return nil
}
user.Username = tools.RandomString(8)
user.Username = str.RandomString(8)
user.Password = hash
if user.Email == "" {
user.Email = str.RandomString(8) + "@example.com"
}
err = facades.Orm().Query().Save(&user)
if err != nil {
color.Redln("保存管理员信息失败")
color.Red().Printfln(translate.Get("commands.panel.getInfo.adminSaveFail"))
return nil
}
port, err := tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
if err != nil {
color.Redln("获取面板端口失败")
color.Red().Printfln(translate.Get("commands.panel.portFail"))
return nil
}
ip, err := tools.GetPublicIP()
if err != nil {
ip = "127.0.0.1"
}
protocol := "http"
if facades.Config().GetBool("panel.ssl") {
protocol = "https"
}
color.Greenln("用户名: " + user.Username)
color.Greenln("密码: " + password)
color.Greenln("面板端口: " + port)
color.Greenln("面板入口: " + facades.Config().GetString("http.entrance"))
color.Green().Printfln(translate.Get("commands.panel.getInfo.username") + ": " + user.Username)
color.Green().Printfln(translate.Get("commands.panel.getInfo.password") + ": " + password)
color.Green().Printfln(translate.Get("commands.panel.port") + ": " + port)
color.Green().Printfln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("http.entrance"))
color.Green().Printfln(translate.Get("commands.panel.getInfo.address") + ": " + protocol + "://" + ip + ":" + port + facades.Config().GetString("http.entrance"))
case "getPort":
port, err := tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
if err != nil {
color.Redln("获取面板端口失败")
color.Red().Printfln(translate.Get("commands.panel.portFail"))
return nil
}
color.Greenln("面板端口: " + port)
color.Green().Printfln(translate.Get("commands.panel.port") + ": " + port)
case "getEntrance":
color.Greenln("面板入口: " + facades.Config().GetString("http.entrance"))
color.Green().Printfln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("http.entrance"))
case "deleteEntrance":
oldEntrance, err := tools.Exec(`cat /www/panel/panel.conf | grep APP_ENTRANCE | awk -F '=' '{print $2}' | tr -d '\n'`)
oldEntrance, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_ENTRANCE | awk -F '=' '{print $2}' | tr -d '\n'`)
if err != nil {
color.Redln("获取面板入口失败")
color.Red().Printfln(translate.Get("commands.panel.deleteEntrance.fail"))
return nil
}
if _, err = tools.Exec("sed -i 's!APP_ENTRANCE=" + oldEntrance + "!APP_ENTRANCE=/!g' /www/panel/panel.conf"); err != nil {
color.Redln("删除面板入口失败")
if _, err = shell.Execf("sed -i 's!APP_ENTRANCE=" + oldEntrance + "!APP_ENTRANCE=/!g' /www/panel/panel.conf"); err != nil {
color.Red().Printfln(translate.Get("commands.panel.deleteEntrance.fail"))
return nil
}
color.Greenln("删除面板入口成功")
color.Green().Printfln(translate.Get("commands.panel.deleteEntrance.success"))
case "writePlugin":
slug := arg1
version := arg2
if len(slug) == 0 || len(version) == 0 {
color.Redln("参数错误")
color.Red().Printfln(translate.Get("commands.panel.writePlugin.paramFail"))
return nil
}
@@ -177,31 +209,31 @@ func (receiver *Panel) Handle(ctx console.Context) error {
})
if err != nil {
color.Redln("写入插件安装状态失败")
color.Red().Printfln(translate.Get("commands.panel.writePlugin.fail"))
return nil
}
color.Greenln("写入插件安装状态成功")
color.Green().Printfln(translate.Get("commands.panel.writePlugin.success"))
case "deletePlugin":
slug := arg1
if len(slug) == 0 {
color.Redln("参数错误")
color.Red().Printfln(translate.Get("commands.panel.deletePlugin.paramFail"))
return nil
}
_, err := facades.Orm().Query().Where("slug", slug).Delete(&models.Plugin{})
if err != nil {
color.Redln("移除插件安装状态失败")
color.Red().Printfln(translate.Get("commands.panel.deletePlugin.fail"))
return nil
}
color.Greenln("移除插件安装状态成功")
color.Green().Printfln(translate.Get("commands.panel.deletePlugin.success"))
case "writeMysqlPassword":
password := arg1
if len(password) == 0 {
color.Redln("参数错误")
color.Red().Printfln(translate.Get("commands.panel.writeMysqlPassword.paramFail"))
return nil
}
@@ -213,20 +245,20 @@ func (receiver *Panel) Handle(ctx console.Context) error {
})
if err != nil {
color.Redln("写入MySQL root密码失败")
color.Red().Printfln(translate.Get("commands.panel.writeMysqlPassword.fail"))
return nil
}
color.Greenln("写入MySQL root密码成功")
color.Green().Printfln(translate.Get("commands.panel.writeMysqlPassword.success"))
case "cleanTask":
_, err := facades.Orm().Query().Model(&models.Task{}).Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).Update("status", models.TaskStatusFailed)
if err != nil {
color.Redln("清理任务失败")
color.Red().Printfln(translate.Get("commands.panel.cleanTask.fail"))
return nil
}
color.Greenln("清理任务成功")
color.Green().Printfln(translate.Get("commands.panel.cleanTask.success"))
case "backup":
backupType := arg1
@@ -235,37 +267,37 @@ func (receiver *Panel) Handle(ctx console.Context) error {
save := arg4
hr := `+----------------------------------------------------`
if len(backupType) == 0 || len(name) == 0 || len(path) == 0 || len(save) == 0 {
color.Redln("参数错误")
color.Red().Printfln(translate.Get("commands.panel.backup.paramFail"))
return nil
}
color.Greenln(hr)
color.Greenln("★ 开始备份 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
color.Green().Printfln(hr)
color.Green().Printfln("★ " + translate.Get("commands.panel.backup.start") + " [" + carbon.Now().ToDateTimeString() + "]")
color.Green().Printfln(hr)
if !tools.Exists(path) {
if err := tools.Mkdir(path, 0644); err != nil {
color.Redln("|-创建备份目录失败: " + err.Error())
if !io.Exists(path) {
if err := io.Mkdir(path, 0644); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.backupDirFail") + ": " + err.Error())
return nil
}
}
switch backupType {
case "website":
color.Yellowln("|-目标网站: " + name)
color.Yellow().Printfln("|-" + translate.Get("commands.panel.backup.targetSite") + ": " + name)
var website models.Website
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil {
color.Redln("|-网站不存在")
color.Greenln(hr)
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.siteNotExist"))
color.Green().Printfln(hr)
return nil
}
backupFile := path + "/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".zip"
if _, err := tools.Exec(`cd '` + website.Path + `' && zip -r '` + backupFile + `' .`); err != nil {
color.Redln("|-备份失败: " + err.Error())
if _, err := shell.Execf(`cd '` + website.Path + `' && zip -r '` + backupFile + `' .`); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.backupFail") + ": " + err.Error())
return nil
}
color.Greenln("|-备份成功")
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.backupSuccess"))
case "mysql":
rootPassword := services.NewSettingImpl().Get(models.SettingKeyMysqlRootPassword)
@@ -273,81 +305,81 @@ func (receiver *Panel) Handle(ctx console.Context) error {
err := os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
color.Redln("|-备份MySQL数据库失败: " + err.Error())
color.Greenln(hr)
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.mysqlBackupFail") + ": " + err.Error())
color.Green().Printfln(hr)
return nil
}
color.Greenln("|-目标MySQL数据库: " + name)
color.Greenln("|-开始导出")
if _, err = tools.Exec(`mysqldump -uroot ` + name + ` > /tmp/` + backupFile + ` 2>&1`); err != nil {
color.Redln("|-导出失败: " + err.Error())
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.targetMysql") + ": " + name)
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startExport"))
if _, err = shell.Execf(`mysqldump -uroot ` + name + ` > /tmp/` + backupFile + ` 2>&1`); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.exportFail") + ": " + err.Error())
return nil
}
color.Greenln("|-导出成功")
color.Greenln("|-开始压缩")
if _, err = tools.Exec("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil {
color.Redln("|-压缩失败: " + err.Error())
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.exportSuccess"))
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startCompress"))
if _, err = shell.Execf("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error())
return nil
}
if err := tools.Remove("/tmp/" + backupFile); err != nil {
color.Redln("|-删除失败: " + err.Error())
if err := io.Remove("/tmp/" + backupFile); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.deleteFail") + ": " + err.Error())
return nil
}
color.Greenln("|-压缩成功")
color.Greenln("|-开始移动")
if err := tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
color.Redln("|-移动失败: " + err.Error())
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.compressSuccess"))
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startMove"))
if err := io.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.moveFail") + ": " + err.Error())
return nil
}
color.Greenln("|-移动成功")
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.moveSuccess"))
_ = os.Unsetenv("MYSQL_PWD")
color.Greenln("|-备份成功")
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.success"))
case "postgresql":
backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
check, err := tools.Exec(`su - postgres -c "psql -l" 2>&1`)
check, err := shell.Execf(`su - postgres -c "psql -l" 2>&1`)
if err != nil {
color.Redln("|-获取数据库失败: " + err.Error())
color.Greenln(hr)
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.databaseGetFail") + ": " + err.Error())
color.Green().Printfln(hr)
return nil
}
if strings.Contains(check, name) {
color.Redln("|-数据库不存在")
color.Greenln(hr)
if !strings.Contains(check, name) {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.databaseNotExist"))
color.Green().Printfln(hr)
return nil
}
color.Greenln("|-目标PostgreSQL数据库: " + name)
color.Greenln("|-开始导出")
if _, err = tools.Exec(`su - postgres -c "pg_dump '` + name + `'" > /tmp/` + backupFile + ` 2>&1`); err != nil {
color.Redln("|-导出失败: " + err.Error())
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.targetPostgres") + ": " + name)
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startExport"))
if _, err = shell.Execf(`su - postgres -c "pg_dump '` + name + `'" > /tmp/` + backupFile + ` 2>&1`); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.exportFail") + ": " + err.Error())
return nil
}
color.Greenln("|-导出成功")
color.Greenln("|-开始压缩")
if _, err = tools.Exec("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil {
color.Redln("|-压缩失败: " + err.Error())
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.exportSuccess"))
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startCompress"))
if _, err = shell.Execf("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error())
return nil
}
if err := tools.Remove("/tmp/" + backupFile); err != nil {
color.Redln("|-删除失败: " + err.Error())
if err := io.Remove("/tmp/" + backupFile); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.deleteFail") + ": " + err.Error())
return nil
}
color.Greenln("|-压缩成功")
color.Greenln("|-开始移动")
if err := tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
color.Redln("|-移动失败: " + err.Error())
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.compressSuccess"))
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.startMove"))
if err := io.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.moveFail") + ": " + err.Error())
return nil
}
color.Greenln("|-移动成功")
color.Greenln("|-备份成功")
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.moveSuccess"))
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.success"))
}
color.Greenln(hr)
color.Green().Printfln(hr)
files, err := os.ReadDir(path)
if err != nil {
color.Redln("|-清理失败: " + err.Error())
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.cleanupFail") + ": " + err.Error())
return nil
}
var filteredFiles []os.FileInfo
@@ -365,60 +397,60 @@ func (receiver *Panel) Handle(ctx console.Context) error {
})
for i := cast.ToInt(save); i < len(filteredFiles); i++ {
fileToDelete := filepath.Join(path, filteredFiles[i].Name())
color.Yellowln("|-清理备份: " + fileToDelete)
if err := tools.Remove(fileToDelete); err != nil {
color.Redln("|-清理失败: " + err.Error())
color.Yellow().Printfln("|-" + translate.Get("commands.panel.backup.cleanBackup") + ": " + fileToDelete)
if err := io.Remove(fileToDelete); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.backup.cleanupFail") + ": " + err.Error())
return nil
}
}
color.Greenln("|-清理完成")
color.Greenln(hr)
color.Greenln("☆ 备份完成 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
color.Green().Printfln("|-" + translate.Get("commands.panel.backup.cleanupSuccess"))
color.Green().Printfln(hr)
color.Green().Printfln("☆ " + translate.Get("commands.panel.backup.success") + " [" + carbon.Now().ToDateTimeString() + "]")
color.Green().Printfln(hr)
case "cutoff":
name := arg1
save := arg2
hr := `+----------------------------------------------------`
if len(name) == 0 || len(save) == 0 {
color.Redln("参数错误")
color.Red().Printfln(translate.Get("commands.panel.cutoff.paramFail"))
return nil
}
color.Greenln(hr)
color.Greenln("★ 开始切割 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
color.Green().Printfln(hr)
color.Green().Printfln("★ " + translate.Get("commands.panel.cutoff.start") + " [" + carbon.Now().ToDateTimeString() + "]")
color.Green().Printfln(hr)
color.Yellowln("|-目标网站: " + name)
color.Yellow().Printfln("|-" + translate.Get("commands.panel.cutoff.targetSite") + ": " + name)
var website models.Website
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil {
color.Redln("|-网站不存在")
color.Greenln(hr)
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.siteNotExist"))
color.Green().Printfln(hr)
return nil
}
logPath := "/www/wwwlogs/" + website.Name + ".log"
if !tools.Exists(logPath) {
color.Redln("|-日志文件不存在")
color.Greenln(hr)
if !io.Exists(logPath) {
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.logNotExist"))
color.Green().Printfln(hr)
return nil
}
backupPath := "/www/wwwlogs/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".log.zip"
if _, err := tools.Exec(`cd /www/wwwlogs && zip -r ` + backupPath + ` ` + website.Name + ".log"); err != nil {
color.Redln("|-备份失败: " + err.Error())
if _, err := shell.Execf(`cd /www/wwwlogs && zip -r ` + backupPath + ` ` + website.Name + ".log"); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.backupFail") + ": " + err.Error())
return nil
}
if _, err := tools.Exec(`echo "" > ` + logPath); err != nil {
color.Redln("|-清空失败: " + err.Error())
if _, err := shell.Execf(`echo "" > ` + logPath); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.clearFail") + ": " + err.Error())
return nil
}
color.Greenln("|-切割成功")
color.Green().Printfln("|-" + translate.Get("commands.panel.cutoff.cutSuccess"))
color.Greenln(hr)
color.Green().Printfln(hr)
files, err := os.ReadDir("/www/wwwlogs")
if err != nil {
color.Redln("|-清理失败: " + err.Error())
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupFail") + ": " + err.Error())
return nil
}
var filteredFiles []os.FileInfo
@@ -436,37 +468,37 @@ func (receiver *Panel) Handle(ctx console.Context) error {
})
for i := cast.ToInt(save); i < len(filteredFiles); i++ {
fileToDelete := filepath.Join("/www/wwwlogs", filteredFiles[i].Name())
color.Yellowln("|-清理日志: " + fileToDelete)
if err := tools.Remove(fileToDelete); err != nil {
color.Redln("|-清理失败: " + err.Error())
color.Yellow().Printfln("|-" + translate.Get("commands.panel.cutoff.clearLog") + ": " + fileToDelete)
if err := io.Remove(fileToDelete); err != nil {
color.Red().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupFail") + ": " + err.Error())
return nil
}
}
color.Greenln("|-清理完成")
color.Greenln(hr)
color.Greenln("☆ 切割完成 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
color.Green().Printfln("|-" + translate.Get("commands.panel.cutoff.cleanupSuccess"))
color.Green().Printfln(hr)
color.Green().Printfln("☆ " + translate.Get("commands.panel.cutoff.end") + " [" + carbon.Now().ToDateTimeString() + "]")
color.Green().Printfln(hr)
case "writeSite":
name := arg1
status := cast.ToBool(arg2)
path := ctx.Argument(3)
php := cast.ToInt(ctx.Argument(4))
path := arg3
php := cast.ToInt(arg4)
ssl := cast.ToBool(ctx.Argument(5))
if len(name) == 0 || len(path) == 0 {
color.Redln("参数错误")
color.Red().Printfln(translate.Get("commands.panel.writeSite.paramFail"))
return nil
}
var website models.Website
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err == nil {
color.Redln("网站已存在")
color.Red().Printfln(translate.Get("commands.panel.writeSite.siteExist"))
return nil
}
_, err := os.Stat(path)
if os.IsNotExist(err) {
color.Redln("网站目录不存在")
color.Red().Printfln(translate.Get("commands.panel.writeSite.pathNotExist"))
return nil
}
@@ -478,32 +510,32 @@ func (receiver *Panel) Handle(ctx console.Context) error {
Ssl: ssl,
})
if err != nil {
color.Redln("写入网站失败")
color.Red().Printfln(translate.Get("commands.panel.writeSite.fail"))
return nil
}
color.Greenln("写入网站成功")
color.Green().Printfln(translate.Get("commands.panel.writeSite.success"))
case "deleteSite":
name := arg1
if len(name) == 0 {
color.Redln("参数错误")
color.Red().Printfln(translate.Get("commands.panel.deleteSite.paramFail"))
return nil
}
_, err := facades.Orm().Query().Where("name", name).Delete(&models.Website{})
if err != nil {
color.Redln("删除网站失败")
color.Red().Printfln(translate.Get("commands.panel.deleteSite.fail"))
return nil
}
color.Greenln("删除网站成功")
color.Green().Printfln(translate.Get("commands.panel.deleteSite.success"))
case "writeSetting":
key := arg1
value := arg2
if len(key) == 0 || len(value) == 0 {
color.Redln("参数错误")
color.Red().Printfln(translate.Get("commands.panel.writeSetting.paramFail"))
return nil
}
@@ -514,16 +546,16 @@ func (receiver *Panel) Handle(ctx console.Context) error {
Value: value,
})
if err != nil {
color.Redln("写入设置失败")
color.Red().Printfln(translate.Get("commands.panel.writeSetting.fail"))
return nil
}
color.Greenln("写入设置成功")
color.Green().Printfln(translate.Get("commands.panel.writeSetting.success"))
case "getSetting":
key := arg1
if len(key) == 0 {
color.Redln("参数错误")
color.Red().Printfln(translate.Get("commands.panel.getSetting.paramFail"))
return nil
}
@@ -537,39 +569,165 @@ func (receiver *Panel) Handle(ctx console.Context) error {
case "deleteSetting":
key := arg1
if len(key) == 0 {
color.Redln("参数错误")
color.Red().Printfln(translate.Get("commands.panel.deleteSetting.paramFail"))
return nil
}
_, err := facades.Orm().Query().Where("key", key).Delete(&models.Setting{})
if err != nil {
color.Redln("删除设置失败")
color.Red().Printfln(translate.Get("commands.panel.deleteSetting.fail"))
return nil
}
color.Greenln("删除设置成功")
color.Green().Printfln(translate.Get("commands.panel.deleteSetting.success"))
case "addSite":
name := arg1
domain := arg2
port := arg3
path := arg4
php := arg5
if len(name) == 0 || len(domain) == 0 || len(port) == 0 || len(path) == 0 {
color.Red().Printfln(translate.Get("commands.panel.addSite.paramFail"))
return nil
}
domains := strings.Split(domain, ",")
ports := strings.Split(port, ",")
if len(domains) == 0 || len(ports) == 0 {
color.Red().Printfln(translate.Get("commands.panel.addSite.paramFail"))
return nil
}
var uintPorts []uint
for _, p := range ports {
uintPorts = append(uintPorts, cast.ToUint(p))
}
website := services.NewWebsiteImpl()
id, err := website.GetIDByName(name)
if err != nil {
color.Red().Printfln(err.Error())
return nil
}
if id != 0 {
color.Red().Printfln(translate.Get("commands.panel.addSite.siteExist"))
return nil
}
_, err = website.Add(types.WebsiteAdd{
Name: name,
Status: true,
Domains: domains,
Ports: uintPorts,
Path: path,
Php: php,
Ssl: false,
Db: false,
})
if err != nil {
color.Red().Printfln(err.Error())
return nil
}
color.Green().Printfln(translate.Get("commands.panel.addSite.success"))
case "removeSite":
name := arg1
if len(name) == 0 {
color.Red().Printfln(translate.Get("commands.panel.removeSite.paramFail"))
return nil
}
website := services.NewWebsiteImpl()
id, err := website.GetIDByName(name)
if err != nil {
color.Red().Printfln(err.Error())
return nil
}
if id == 0 {
color.Red().Printfln(translate.Get("commands.panel.removeSite.siteNotExist"))
return nil
}
if err = website.Delete(requests.Delete{ID: id}); err != nil {
color.Red().Printfln(err.Error())
return nil
}
color.Green().Printfln(translate.Get("commands.panel.removeSite.success"))
case "installPlugin":
slug := arg1
if len(slug) == 0 {
color.Red().Printfln(translate.Get("commands.panel.installPlugin.paramFail"))
return nil
}
plugin := services.NewPluginImpl()
if err := plugin.Install(slug); err != nil {
color.Red().Printfln(err.Error())
return nil
}
color.Green().Printfln(translate.Get("commands.panel.installPlugin.success"))
case "uninstallPlugin":
slug := arg1
if len(slug) == 0 {
color.Red().Printfln(translate.Get("commands.panel.uninstallPlugin.paramFail"))
return nil
}
plugin := services.NewPluginImpl()
if err := plugin.Uninstall(slug); err != nil {
color.Red().Printfln(err.Error())
return nil
}
color.Green().Printfln(translate.Get("commands.panel.uninstallPlugin.success"))
case "updatePlugin":
slug := arg1
if len(slug) == 0 {
color.Red().Printfln(translate.Get("commands.panel.updatePlugin.paramFail"))
return nil
}
plugin := services.NewPluginImpl()
if err := plugin.Update(slug); err != nil {
color.Red().Printfln(err.Error())
return nil
}
color.Green().Printfln(translate.Get("commands.panel.updatePlugin.success"))
default:
color.Yellowln(facades.Config().GetString("panel.name") + "命令行工具 - " + facades.Config().GetString("panel.version"))
color.Greenln("请使用以下命令")
color.Greenln("panel update 更新 / 修复面板到最新版本")
color.Greenln("panel getInfo 重新初始化面板账号信息")
color.Greenln("panel getPort 获取面板访问端口")
color.Greenln("panel getEntrance 获取面板访问入口")
color.Greenln("panel deleteEntrance 删除面板访问入口")
color.Greenln("panel cleanTask 清理面板运行中和等待中的任务[任务卡住时使用]")
color.Greenln("panel backup {website/mysql/postgresql} {name} {path} {save_copies} 备份网站 / MySQL数据库 / PostgreSQL数据库到指定目录并保留指定数量")
color.Greenln("panel cutoff {website_name} {save_copies} 切割网站日志并保留指定数量")
color.Redln("以下命令请在开发者指导下使用:")
color.Yellowln("panel init 初始化面板")
color.Yellowln("panel writePlugin {slug} {version} 写入插件安装状态")
color.Yellowln("panel deletePlugin {slug} 移除插件安装状态")
color.Yellowln("panel writeMysqlPassword {password} 写入MySQL root密码")
color.Yellowln("panel writeSite {name} {status} {path} {php} {ssl} 写入网站数据到面板")
color.Yellowln("panel deleteSite {name} 删除面板网站数据")
color.Yellowln("panel getSetting {name} 获取面板设置数据")
color.Yellowln("panel writeSetting {name} {value} 写入 / 更新面板设置数据")
color.Yellowln("panel deleteSetting {name} 删除面板设置数据")
color.Yellow().Printfln(facades.Config().GetString("panel.name") + " - " + translate.Get("commands.panel.tool") + " - " + facades.Config().GetString("panel.version"))
color.Green().Printfln(translate.Get("commands.panel.use") + "")
color.Green().Printfln("panel update " + translate.Get("commands.panel.update.description"))
color.Green().Printfln("panel getInfo " + translate.Get("commands.panel.getInfo.description"))
color.Green().Printfln("panel getPort " + translate.Get("commands.panel.getPort.description"))
color.Green().Printfln("panel getEntrance " + translate.Get("commands.panel.getEntrance.description"))
color.Green().Printfln("panel deleteEntrance " + translate.Get("commands.panel.deleteEntrance.description"))
color.Green().Printfln("panel cleanTask " + translate.Get("commands.panel.cleanTask.description"))
color.Green().Printfln("panel backup {website/mysql/postgresql} {name} {path} {save_copies} " + translate.Get("commands.panel.backup.description"))
color.Green().Printfln("panel cutoff {website_name} {save_copies} " + translate.Get("commands.panel.cutoff.description"))
color.Green().Printfln("panel installPlugin {slug} " + translate.Get("commands.panel.installPlugin.description"))
color.Green().Printfln("panel uninstallPlugin {slug} " + translate.Get("commands.panel.uninstallPlugin.description"))
color.Green().Printfln("panel updatePlugin {slug} " + translate.Get("commands.panel.updatePlugin.description"))
color.Green().Printfln("panel addSite {name} {domain} {port} {path} {php} " + translate.Get("commands.panel.addSite.description"))
color.Green().Printfln("panel removeSite {name} " + translate.Get("commands.panel.removeSite.description"))
color.Red().Printfln(translate.Get("commands.panel.forDeveloper") + ":")
color.Yellow().Printfln("panel init " + translate.Get("commands.panel.init.description"))
color.Yellow().Printfln("panel writePlugin {slug} {version} " + translate.Get("commands.panel.writePlugin.description"))
color.Yellow().Printfln("panel deletePlugin {slug} " + translate.Get("commands.panel.deletePlugin.description"))
color.Yellow().Printfln("panel writeMysqlPassword {password} " + translate.Get("commands.panel.writeMysqlPassword.description"))
color.Yellow().Printfln("panel writeSite {name} {status} {path} {php} {ssl} " + translate.Get("commands.panel.writeSite.description"))
color.Yellow().Printfln("panel deleteSite {name} " + translate.Get("commands.panel.deleteSite.description"))
color.Yellow().Printfln("panel getSetting {name} " + translate.Get("commands.panel.getSetting.description"))
color.Yellow().Printfln("panel writeSetting {name} {value} " + translate.Get("commands.panel.writeSetting.description"))
color.Yellow().Printfln("panel deleteSetting {name} " + translate.Get("commands.panel.deleteSetting.description"))
}
return nil
+79
View File
@@ -0,0 +1,79 @@
package commands
import (
"context"
"runtime"
"runtime/debug"
"github.com/goravel/framework/contracts/console"
"github.com/goravel/framework/contracts/console/command"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/types"
)
// PanelTask 面板每日任务
type PanelTask struct {
}
// Signature The name and signature of the console command.
func (receiver *PanelTask) Signature() string {
return "panel:task"
}
// Description The console command description.
func (receiver *PanelTask) Description() string {
return facades.Lang(context.Background()).Get("commands.panel:task.description")
}
// Extend The console command extend.
func (receiver *PanelTask) Extend() command.Extend {
return command.Extend{
Category: "panel",
}
}
// Handle Execute the console command.
func (receiver *PanelTask) Handle(console.Context) error {
types.Status = types.StatusMaintain
// 优化数据库
if _, err := facades.Orm().Query().Exec("VACUUM"); err != nil {
types.Status = types.StatusFailed
facades.Log().Tags("面板", "每日任务").
With(map[string]any{
"error": err.Error(),
}).Error("优化面板数据库失败")
return err
}
// 备份面板
if err := io.Archive([]string{"/www/panel"}, "/www/backup/panel/panel-"+carbon.Now().ToShortDateTimeString()+".zip"); err != nil {
types.Status = types.StatusFailed
facades.Log().Tags("面板", "每日任务").
With(map[string]any{
"error": err.Error(),
}).Error("备份面板失败")
return err
}
// 清理 7 天前的备份
if _, err := shell.Execf(`find /www/backup/panel -mtime +7 -name "*.zip" -exec rm -rf {} \;`); err != nil {
types.Status = types.StatusFailed
facades.Log().Tags("面板", "每日任务").
With(map[string]any{
"error": err.Error(),
}).Error("清理面板备份失败")
return err
}
// 回收内存
runtime.GC()
debug.FreeOSMemory()
types.Status = types.StatusNormal
return nil
}
+4 -2
View File
@@ -5,7 +5,7 @@ import (
"github.com/goravel/framework/contracts/schedule"
"github.com/goravel/framework/facades"
"panel/app/console/commands"
"github.com/TheTNB/panel/app/console/commands"
)
type Kernel struct {
@@ -14,7 +14,8 @@ type Kernel struct {
func (kernel *Kernel) Schedule() []schedule.Event {
return []schedule.Event{
facades.Schedule().Command("panel:monitoring").EveryMinute().SkipIfStillRunning(),
facades.Schedule().Command("panel:cert-renew").Daily().SkipIfStillRunning(),
facades.Schedule().Command("panel:cert-renew").DailyAt("04:00").SkipIfStillRunning(),
facades.Schedule().Command("panel:task").DailyAt("03:30").SkipIfStillRunning(),
}
}
@@ -23,5 +24,6 @@ func (kernel *Kernel) Commands() []console.Command {
&commands.Panel{},
&commands.Monitoring{},
&commands.CertRenew{},
&commands.PanelTask{},
}
}
-76
View File
@@ -1,76 +0,0 @@
package controllers
import (
"os"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/services"
"panel/pkg/tools"
)
type AssetController struct {
setting services.Setting
}
func NewAssetController() *AssetController {
return &AssetController{
setting: services.NewSettingImpl(),
}
}
func (r *AssetController) Index(ctx http.Context) http.Response {
entrance := facades.Config().GetString("http.entrance")
if entrance == "/" {
entrance = ""
}
// 自动纠正 URL 格式
if ctx.Request().Path() == entrance && ctx.Request().Path() != "/" {
return ctx.Response().Redirect(http.StatusMovedPermanently, ctx.Request().Path()+"/")
}
// 拒绝访问非入口文件
if !strings.HasPrefix(ctx.Request().Path(), entrance) {
return ctx.Response().Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound))
}
path := strings.TrimPrefix(ctx.Request().Path(), entrance)
// 设置默认首页
if path == "/" || path == "" {
path = "/index.html"
}
if !tools.Exists("public" + path) {
return ctx.Response().Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound))
}
file, err := os.Open("public" + path)
if err != nil {
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
}
stat, err := file.Stat()
if err != nil {
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
}
if stat.IsDir() {
return ctx.Response().Status(http.StatusForbidden).String(http.StatusText(http.StatusForbidden))
}
return ctx.Response().Header("Cache-Control", "no-cache").File("public" + path)
}
func (r *AssetController) Favicon(ctx http.Context) http.Response {
return ctx.Response().File("public/favicon.png")
}
func (r *AssetController) Robots(ctx http.Context) http.Response {
return ctx.Response().File("public/robots.txt")
}
func (r *AssetController) NotFound(ctx http.Context) http.Response {
return ctx.Response().Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound))
}
+97 -63
View File
@@ -4,17 +4,17 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
requests "panel/app/http/requests/cert"
commonrequests "panel/app/http/requests/common"
responses "panel/app/http/responses/cert"
"panel/app/models"
"panel/app/services"
"panel/pkg/acme"
requests "github.com/TheTNB/panel/app/http/requests/cert"
commonrequests "github.com/TheTNB/panel/app/http/requests/common"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/acme"
)
type CertController struct {
cron services.Cron
cert services.Cert
cron internal.Cron
cert internal.Cert
}
func NewCertController() *CertController {
@@ -28,7 +28,7 @@ func NewCertController() *CertController {
//
// @Summary 获取 CA 提供商
// @Description 获取面板证书管理支持的 CA 提供商
// @Tags 证书管理
// @Tags TLS证书
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
@@ -62,7 +62,7 @@ func (r *CertController) CAProviders(ctx http.Context) http.Response {
//
// @Summary 获取 DNS 提供商
// @Description 获取面板证书管理支持的 DNS 提供商
// @Tags 证书管理
// @Tags TLS证书
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
@@ -73,6 +73,10 @@ func (r *CertController) DNSProviders(ctx http.Context) http.Response {
"name": "DNSPod",
"dns": acme.DnsPod,
},
{
"name": "腾讯云",
"dns": acme.Tencent,
},
{
"name": "阿里云",
"dns": acme.AliYun,
@@ -88,7 +92,7 @@ func (r *CertController) DNSProviders(ctx http.Context) http.Response {
//
// @Summary 获取算法列表
// @Description 获取面板证书管理支持的算法列表
// @Tags 证书管理
// @Tags TLS证书
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
@@ -118,15 +122,15 @@ func (r *CertController) Algorithms(ctx http.Context) http.Response {
//
// @Summary 获取用户列表
// @Description 获取面板证书管理的 ACME 用户列表
// @Tags 证书管理
// @Tags TLS证书
// @Produce json
// @Security BearerToken
// @Param data body commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse{data=responses.CertList}
// @Param data query commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/cert/users [get]
func (r *CertController) UserList(ctx http.Context) http.Response {
var paginateRequest commonrequests.Paginate
sanitize := Sanitize(ctx, &paginateRequest)
sanitize := SanitizeRequest(ctx, &paginateRequest)
if sanitize != nil {
return sanitize
}
@@ -141,9 +145,9 @@ func (r *CertController) UserList(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
return Success(ctx, responses.UserList{
Total: total,
Items: users,
return Success(ctx, http.Json{
"total": total,
"items": users,
})
}
@@ -151,7 +155,7 @@ func (r *CertController) UserList(ctx http.Context) http.Response {
//
// @Summary 添加 ACME 用户
// @Description 添加 ACME 用户到面板证书管理
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
@@ -160,7 +164,7 @@ func (r *CertController) UserList(ctx http.Context) http.Response {
// @Router /panel/cert/users [post]
func (r *CertController) UserStore(ctx http.Context) http.Response {
var storeRequest requests.UserStore
sanitize := Sanitize(ctx, &storeRequest)
sanitize := SanitizeRequest(ctx, &storeRequest)
if sanitize != nil {
return sanitize
}
@@ -180,7 +184,7 @@ func (r *CertController) UserStore(ctx http.Context) http.Response {
//
// @Summary 更新 ACME 用户
// @Description 更新面板证书管理的 ACME 用户
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
@@ -190,7 +194,7 @@ func (r *CertController) UserStore(ctx http.Context) http.Response {
// @Router /panel/cert/users/{id} [put]
func (r *CertController) UserUpdate(ctx http.Context) http.Response {
var updateRequest requests.UserUpdate
sanitize := Sanitize(ctx, &updateRequest)
sanitize := SanitizeRequest(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
@@ -211,7 +215,7 @@ func (r *CertController) UserUpdate(ctx http.Context) http.Response {
//
// @Summary 获取 ACME 用户
// @Description 获取面板证书管理的 ACME 用户
// @Tags 证书管理
// @Tags TLS证书
// @Produce json
// @Security BearerToken
// @Param id path int true "用户 ID"
@@ -219,7 +223,7 @@ func (r *CertController) UserUpdate(ctx http.Context) http.Response {
// @Router /panel/cert/users/{id} [get]
func (r *CertController) UserShow(ctx http.Context) http.Response {
var showAndDestroyRequest requests.UserShowAndDestroy
sanitize := Sanitize(ctx, &showAndDestroyRequest)
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
if sanitize != nil {
return sanitize
}
@@ -240,7 +244,7 @@ func (r *CertController) UserShow(ctx http.Context) http.Response {
//
// @Summary 删除 ACME 用户
// @Description 删除面板证书管理的 ACME 用户
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
@@ -249,7 +253,7 @@ func (r *CertController) UserShow(ctx http.Context) http.Response {
// @Router /panel/cert/users/{id} [delete]
func (r *CertController) UserDestroy(ctx http.Context) http.Response {
var showAndDestroyRequest requests.UserShowAndDestroy
sanitize := Sanitize(ctx, &showAndDestroyRequest)
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
if sanitize != nil {
return sanitize
}
@@ -270,15 +274,15 @@ func (r *CertController) UserDestroy(ctx http.Context) http.Response {
//
// @Summary 获取 DNS 接口列表
// @Description 获取面板证书管理的 DNS 接口列表
// @Tags 证书管理
// @Tags TLS证书
// @Produce json
// @Security BearerToken
// @Param data body commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse{data=responses.DNSList}
// @Param data query commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/cert/dns [get]
func (r *CertController) DNSList(ctx http.Context) http.Response {
var paginateRequest commonrequests.Paginate
sanitize := Sanitize(ctx, &paginateRequest)
sanitize := SanitizeRequest(ctx, &paginateRequest)
if sanitize != nil {
return sanitize
}
@@ -293,9 +297,9 @@ func (r *CertController) DNSList(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
return Success(ctx, responses.DNSList{
Total: total,
Items: dns,
return Success(ctx, http.Json{
"total": total,
"items": dns,
})
}
@@ -303,7 +307,7 @@ func (r *CertController) DNSList(ctx http.Context) http.Response {
//
// @Summary 添加 DNS 接口
// @Description 添加 DNS 接口到面板证书管理
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
@@ -312,7 +316,7 @@ func (r *CertController) DNSList(ctx http.Context) http.Response {
// @Router /panel/cert/dns [post]
func (r *CertController) DNSStore(ctx http.Context) http.Response {
var storeRequest requests.DNSStore
sanitize := Sanitize(ctx, &storeRequest)
sanitize := SanitizeRequest(ctx, &storeRequest)
if sanitize != nil {
return sanitize
}
@@ -332,7 +336,7 @@ func (r *CertController) DNSStore(ctx http.Context) http.Response {
//
// @Summary 获取 DNS 接口
// @Description 获取面板证书管理的 DNS 接口
// @Tags 证书管理
// @Tags TLS证书
// @Produce json
// @Security BearerToken
// @Param id path int true "DNS 接口 ID"
@@ -340,7 +344,7 @@ func (r *CertController) DNSStore(ctx http.Context) http.Response {
// @Router /panel/cert/dns/{id} [get]
func (r *CertController) DNSShow(ctx http.Context) http.Response {
var showAndDestroyRequest requests.DNSShowAndDestroy
sanitize := Sanitize(ctx, &showAndDestroyRequest)
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
if sanitize != nil {
return sanitize
}
@@ -361,7 +365,7 @@ func (r *CertController) DNSShow(ctx http.Context) http.Response {
//
// @Summary 更新 DNS 接口
// @Description 更新面板证书管理的 DNS 接口
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
@@ -371,7 +375,7 @@ func (r *CertController) DNSShow(ctx http.Context) http.Response {
// @Router /panel/cert/dns/{id} [put]
func (r *CertController) DNSUpdate(ctx http.Context) http.Response {
var updateRequest requests.DNSUpdate
sanitize := Sanitize(ctx, &updateRequest)
sanitize := SanitizeRequest(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
@@ -392,7 +396,7 @@ func (r *CertController) DNSUpdate(ctx http.Context) http.Response {
//
// @Summary 删除 DNS 接口
// @Description 删除面板证书管理的 DNS 接口
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
@@ -401,7 +405,7 @@ func (r *CertController) DNSUpdate(ctx http.Context) http.Response {
// @Router /panel/cert/dns/{id} [delete]
func (r *CertController) DNSDestroy(ctx http.Context) http.Response {
var showAndDestroyRequest requests.DNSShowAndDestroy
sanitize := Sanitize(ctx, &showAndDestroyRequest)
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
if sanitize != nil {
return sanitize
}
@@ -422,15 +426,15 @@ func (r *CertController) DNSDestroy(ctx http.Context) http.Response {
//
// @Summary 获取证书列表
// @Description 获取面板证书管理的证书列表
// @Tags 证书管理
// @Tags TLS证书
// @Produce json
// @Security BearerToken
// @Param data body commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse{data=responses.CertList}
// @Param data query commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/cert/certs [get]
func (r *CertController) CertList(ctx http.Context) http.Response {
var paginateRequest commonrequests.Paginate
sanitize := Sanitize(ctx, &paginateRequest)
sanitize := SanitizeRequest(ctx, &paginateRequest)
if sanitize != nil {
return sanitize
}
@@ -445,9 +449,9 @@ func (r *CertController) CertList(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
return Success(ctx, responses.CertList{
Total: total,
Items: certs,
return Success(ctx, http.Json{
"total": total,
"items": certs,
})
}
@@ -455,7 +459,7 @@ func (r *CertController) CertList(ctx http.Context) http.Response {
//
// @Summary 添加证书
// @Description 添加证书到面板证书管理
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
@@ -464,7 +468,7 @@ func (r *CertController) CertList(ctx http.Context) http.Response {
// @Router /panel/cert/certs [post]
func (r *CertController) CertStore(ctx http.Context) http.Response {
var storeRequest requests.CertStore
sanitize := Sanitize(ctx, &storeRequest)
sanitize := SanitizeRequest(ctx, &storeRequest)
if sanitize != nil {
return sanitize
}
@@ -484,7 +488,7 @@ func (r *CertController) CertStore(ctx http.Context) http.Response {
//
// @Summary 更新证书
// @Description 更新面板证书管理的证书
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
@@ -494,7 +498,7 @@ func (r *CertController) CertStore(ctx http.Context) http.Response {
// @Router /panel/cert/certs/{id} [put]
func (r *CertController) CertUpdate(ctx http.Context) http.Response {
var updateRequest requests.CertUpdate
sanitize := Sanitize(ctx, &updateRequest)
sanitize := SanitizeRequest(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
@@ -515,7 +519,7 @@ func (r *CertController) CertUpdate(ctx http.Context) http.Response {
//
// @Summary 获取证书
// @Description 获取面板证书管理的证书
// @Tags 证书管理
// @Tags TLS证书
// @Produce json
// @Security BearerToken
// @Param id path int true "证书 ID"
@@ -523,7 +527,7 @@ func (r *CertController) CertUpdate(ctx http.Context) http.Response {
// @Router /panel/cert/certs/{id} [get]
func (r *CertController) CertShow(ctx http.Context) http.Response {
var showAndDestroyRequest requests.CertShowAndDestroy
sanitize := Sanitize(ctx, &showAndDestroyRequest)
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
if sanitize != nil {
return sanitize
}
@@ -544,7 +548,7 @@ func (r *CertController) CertShow(ctx http.Context) http.Response {
//
// @Summary 删除证书
// @Description 删除面板证书管理的证书
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
@@ -553,7 +557,7 @@ func (r *CertController) CertShow(ctx http.Context) http.Response {
// @Router /panel/cert/certs/{id} [delete]
func (r *CertController) CertDestroy(ctx http.Context) http.Response {
var showAndDestroyRequest requests.CertShowAndDestroy
sanitize := Sanitize(ctx, &showAndDestroyRequest)
sanitize := SanitizeRequest(ctx, &showAndDestroyRequest)
if sanitize != nil {
return sanitize
}
@@ -574,7 +578,7 @@ func (r *CertController) CertDestroy(ctx http.Context) http.Response {
//
// @Summary 签发证书
// @Description 签发面板证书管理的证书
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
@@ -583,7 +587,7 @@ func (r *CertController) CertDestroy(ctx http.Context) http.Response {
// @Router /panel/cert/obtain [post]
func (r *CertController) Obtain(ctx http.Context) http.Response {
var obtainRequest requests.Obtain
sanitize := Sanitize(ctx, &obtainRequest)
sanitize := SanitizeRequest(ctx, &obtainRequest)
if sanitize != nil {
return sanitize
}
@@ -616,7 +620,7 @@ func (r *CertController) Obtain(ctx http.Context) http.Response {
//
// @Summary 续签证书
// @Description 续签面板证书管理的证书
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
@@ -625,7 +629,7 @@ func (r *CertController) Obtain(ctx http.Context) http.Response {
// @Router /panel/cert/renew [post]
func (r *CertController) Renew(ctx http.Context) http.Response {
var renewRequest requests.Renew
sanitize := Sanitize(ctx, &renewRequest)
sanitize := SanitizeRequest(ctx, &renewRequest)
if sanitize != nil {
return sanitize
}
@@ -645,16 +649,16 @@ func (r *CertController) Renew(ctx http.Context) http.Response {
//
// @Summary 获取手动 DNS 记录
// @Description 获取签发证书所需的 DNS 记录
// @Tags 证书管理
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Obtain true "request"
// @Success 200 {object} SuccessResponse{data=map[string]acme.Resolve}
// @Success 200 {object} SuccessResponse{data=[]acme.DNSRecord}
// @Router /panel/cert/manualDNS [post]
func (r *CertController) ManualDNS(ctx http.Context) http.Response {
var obtainRequest requests.Obtain
sanitize := Sanitize(ctx, &obtainRequest)
sanitize := SanitizeRequest(ctx, &obtainRequest)
if sanitize != nil {
return sanitize
}
@@ -669,3 +673,33 @@ func (r *CertController) ManualDNS(ctx http.Context) http.Response {
return Success(ctx, resolves)
}
// Deploy
//
// @Summary 部署证书
// @Description 部署面板证书管理的证书
// @Tags TLS证书
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.CertDeploy true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/cert/deploy [post]
func (r *CertController) Deploy(ctx http.Context) http.Response {
var deployRequest requests.CertDeploy
sanitize := SanitizeRequest(ctx, &deployRequest)
if sanitize != nil {
return sanitize
}
err := r.cert.Deploy(deployRequest.ID, deployRequest.WebsiteID)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "证书管理").With(map[string]any{
"certID": deployRequest.ID,
"error": err.Error(),
}).Info("部署证书失败")
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
File diff suppressed because it is too large Load Diff
+48 -11
View File
@@ -2,25 +2,25 @@ package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
commonrequests "github.com/TheTNB/panel/app/http/requests/common"
)
// SuccessResponse 通用成功响应
type SuccessResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
// ErrorResponse 通用错误响应
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
// Success 响应成功
func Success(ctx http.Context, data any) http.Response {
return ctx.Response().Success().Json(&SuccessResponse{
Code: 0,
Message: "success",
Data: data,
})
@@ -28,22 +28,46 @@ func Success(ctx http.Context, data any) http.Response {
// Error 响应错误
func Error(ctx http.Context, code int, message string) http.Response {
return ctx.Response().Json(http.StatusOK, &ErrorResponse{
Code: code,
Message: "错误: " + message,
return ctx.Response().Json(code, &ErrorResponse{
Message: message,
})
}
// ErrorSystem 响应系统错误
func ErrorSystem(ctx http.Context) http.Response {
return ctx.Response().Json(http.StatusOK, &ErrorResponse{
Code: http.StatusInternalServerError,
Message: "系统内部错误",
return ctx.Response().Json(http.StatusInternalServerError, &ErrorResponse{
Message: facades.Lang(ctx).Get("errors.internal"),
})
}
// Sanitize 消毒请求参数
func Sanitize(ctx http.Context, request http.FormRequest) http.Response {
func Paginate[T any](ctx http.Context, allItems []T) (pagedItems []T, total int) {
var paginateRequest commonrequests.Paginate
sanitize := SanitizeRequest(ctx, &paginateRequest)
if sanitize != nil {
return []T{}, 0
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
total = len(allItems)
startIndex := (page - 1) * limit
endIndex := page * limit
if total == 0 {
return []T{}, 0
}
if startIndex > total {
return []T{}, total
}
if endIndex > total {
endIndex = total
}
return allItems[startIndex:endIndex], total
}
// SanitizeRequest 消毒请求参数
func SanitizeRequest(ctx http.Context, request http.FormRequest) http.Response {
errors, err := ctx.Request().ValidateRequest(request)
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
@@ -54,3 +78,16 @@ func Sanitize(ctx http.Context, request http.FormRequest) http.Response {
return nil
}
// Sanitize 消毒参数
func Sanitize(ctx http.Context, rules map[string]string) http.Response {
validator, err := ctx.Request().Validate(rules)
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
return nil
}
+41 -54
View File
@@ -8,14 +8,18 @@ import (
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
)
type CronController struct {
cron services.Cron
setting services.Setting
cron internal.Cron
setting internal.Setting
}
func NewCronController() *CronController {
@@ -48,18 +52,14 @@ func (r *CronController) List(ctx http.Context) http.Response {
// Add 添加计划任务
func (r *CronController) Add(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
if sanitize := Sanitize(ctx, map[string]string{
"name": "required|min_len:1|max_len:255",
"time": "required",
"script": "required",
"type": "required|in:shell,backup,cutoff",
"backup_type": "required_if:type,backup|in:website,mysql,postgresql",
})
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}); sanitize != nil {
return sanitize
}
// 单独验证时间格式
@@ -67,11 +67,11 @@ func (r *CronController) Add(ctx http.Context) http.Response {
return Error(ctx, http.StatusUnprocessableEntity, "时间格式错误")
}
shell := ctx.Request().Input("script")
script := ctx.Request().Input("script")
cronType := ctx.Request().Input("type")
if cronType == "backup" {
backupType := ctx.Request().Input("backup_type")
backupName := ctx.Request().Input("backup_database")
backupName := ctx.Request().Input("database")
if backupType == "website" {
backupName = ctx.Request().Input("website")
}
@@ -80,10 +80,10 @@ func (r *CronController) Add(ctx http.Context) http.Response {
backupPath = r.setting.Get(models.SettingKeyBackupPath) + "/" + backupType
}
backupSave := ctx.Request().InputInt("save", 10)
shell = `#!/bin/bash
script = `#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子Linux面板 - 数据备份脚本
# 耗子面板 - 数据备份脚本
type=` + backupType + `
path=` + backupPath + `
@@ -97,10 +97,10 @@ panel backup ${type} ${name} ${path} ${save} 2>&1
if cronType == "cutoff" {
website := ctx.Request().Input("website")
save := ctx.Request().InputInt("save", 180)
shell = `#!/bin/bash
script = `#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子Linux面板 - 日志切割脚本
# 耗子面板 - 日志切割脚本
name=` + website + `
save=` + cast.ToString(save) + `
@@ -112,17 +112,17 @@ panel cutoff ${name} ${save} 2>&1
shellDir := "/www/server/cron/"
shellLogDir := "/www/server/cron/logs/"
if !tools.Exists(shellDir) {
if !io.Exists(shellDir) {
return Error(ctx, http.StatusInternalServerError, "计划任务目录不存在")
}
if !tools.Exists(shellLogDir) {
if !io.Exists(shellLogDir) {
return Error(ctx, http.StatusInternalServerError, "计划任务日志目录不存在")
}
shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + tools.RandomString(16)
if err = tools.Write(shellDir+shellFile+".sh", shell, 0700); err != nil {
shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + str.RandomString(16)
if err := io.Write(shellDir+shellFile+".sh", script, 0700); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := tools.Exec("dos2unix " + shellDir + shellFile + ".sh"); err != nil {
if out, err := shell.Execf("dos2unix " + shellDir + shellFile + ".sh"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
@@ -134,8 +134,7 @@ panel cutoff ${name} ${save} 2>&1
cron.Shell = shellDir + shellFile + ".sh"
cron.Log = shellLogDir + shellFile + ".log"
err = facades.Orm().Query().Create(&cron)
if err != nil {
if err := facades.Orm().Query().Create(&cron); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{
"error": err.Error(),
}).Info("保存计划任务失败")
@@ -159,26 +158,22 @@ func (r *CronController) Script(ctx http.Context) http.Response {
return Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
}
shell, err := tools.Read(cron.Shell)
script, err := io.Read(cron.Shell)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, shell)
return Success(ctx, script)
}
// Update 更新计划任务
func (r *CronController) Update(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
if sanitize := Sanitize(ctx, map[string]string{
"name": "required|min_len:1|max_len:255",
"time": "required",
"script": "required",
})
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}); sanitize != nil {
return sanitize
}
// 单独验证时间格式
@@ -187,8 +182,7 @@ func (r *CronController) Update(ctx http.Context) http.Response {
}
var cron models.Cron
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil {
return Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
}
@@ -198,18 +192,17 @@ func (r *CronController) Update(ctx http.Context) http.Response {
cron.Time = ctx.Request().Input("time")
cron.Name = ctx.Request().Input("name")
err = facades.Orm().Query().Save(&cron)
if err != nil {
if err := facades.Orm().Query().Save(&cron); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{
"error": err.Error(),
}).Info("更新计划任务失败")
return ErrorSystem(ctx)
}
if err = tools.Write(cron.Shell, ctx.Request().Input("script"), 0644); err != nil {
if err := io.Write(cron.Shell, ctx.Request().Input("script"), 0644); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := tools.Exec("dos2unix " + cron.Shell); err != nil {
if out, err := shell.Execf("dos2unix " + cron.Shell); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
@@ -235,7 +228,7 @@ func (r *CronController) Delete(ctx http.Context) http.Response {
if err := r.cron.DeleteFromSystem(cron); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.Remove(cron.Shell); err != nil {
if err := io.Remove(cron.Shell); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
@@ -251,25 +244,19 @@ func (r *CronController) Delete(ctx http.Context) http.Response {
// Status 更新计划任务状态
func (r *CronController) Status(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
if sanitize := Sanitize(ctx, map[string]string{
"status": "bool",
})
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}); sanitize != nil {
return sanitize
}
var cron models.Cron
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
if err != nil {
if err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron); err != nil {
return Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
}
cron.Status = ctx.Request().InputBool("status")
err = facades.Orm().Query().Save(&cron)
if err != nil {
if err := facades.Orm().Query().Save(&cron); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "计划任务").With(map[string]any{
"error": err.Error(),
}).Info("更新计划任务状态失败")
@@ -295,11 +282,11 @@ func (r *CronController) Log(ctx http.Context) http.Response {
return Error(ctx, http.StatusUnprocessableEntity, "计划任务不存在")
}
if !tools.Exists(cron.Log) {
if !io.Exists(cron.Log) {
return Error(ctx, http.StatusUnprocessableEntity, "日志文件不存在")
}
log, err := tools.Exec("tail -n 1000 " + cron.Log)
log, err := shell.Execf("tail -n 1000 " + cron.Log)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
+511 -4
View File
@@ -1,11 +1,518 @@
package controllers
import (
"fmt"
stdio "io"
stdos "os"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/support/carbon"
requests "github.com/TheTNB/panel/app/http/requests/file"
"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 FileController struct {
// Dependent services
}
func NewFileController() *FileController {
return &FileController{
// Inject services
}
return &FileController{}
}
// Create
//
// @Summary 创建文件/目录
// @Description 创建文件/目录到给定路径
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.NotExist true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/create [post]
func (r *FileController) Create(ctx http.Context) http.Response {
var request requests.NotExist
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
isDir := ctx.Request().InputBool("dir")
if !isDir {
if out, err := shell.Execf("touch " + request.Path); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
if err := io.Mkdir(request.Path, 0755); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
}
r.setPermission(request.Path, 0755, "www", "www")
return Success(ctx, nil)
}
// Content
//
// @Summary 获取文件内容
// @Description 获取给定路径的文件内容
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data query requests.Exist true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/content [get]
func (r *FileController) Content(ctx http.Context) http.Response {
var request requests.Exist
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
fileInfo, err := io.FileInfo(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if fileInfo.IsDir() {
return Error(ctx, http.StatusInternalServerError, "目标路径不是文件")
}
if fileInfo.Size() > 10*1024*1024 {
return Error(ctx, http.StatusInternalServerError, "文件大小超过 10 M,不支持在线编辑")
}
content, err := io.Read(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, content)
}
// Save
//
// @Summary 保存文件内容
// @Description 保存给定路径的文件内容
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Save true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/save [post]
func (r *FileController) Save(ctx http.Context) http.Response {
var request requests.Save
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
fileInfo, err := io.FileInfo(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = io.Write(request.Path, request.Content, fileInfo.Mode()); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Path, 0755, "www", "www")
return Success(ctx, nil)
}
// Delete
//
// @Summary 删除文件/目录
// @Description 删除给定路径的文件/目录
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Exist true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/delete [post]
func (r *FileController) Delete(ctx http.Context) http.Response {
var request requests.Exist
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
if err := io.Remove(request.Path); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// Upload
//
// @Summary 上传文件
// @Description 上传文件到给定路径
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param file formData file true "file"
// @Param path formData string true "path"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/upload [post]
func (r *FileController) Upload(ctx http.Context) http.Response {
var request requests.Upload
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
src, err := request.File.Open()
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
defer src.Close()
if io.Exists(request.Path) && !ctx.Request().InputBool("force") {
return Error(ctx, http.StatusForbidden, "目标路径已存在,是否覆盖?")
}
data, err := stdio.ReadAll(src)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = io.Write(request.Path, string(data), 0755); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Path, 0755, "www", "www")
return Success(ctx, nil)
}
// Move
//
// @Summary 移动文件/目录
// @Description 移动文件/目录到给定路径,等效于重命名
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Move true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/move [post]
func (r *FileController) Move(ctx http.Context) http.Response {
var request requests.Move
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
if io.Exists(request.Target) && !ctx.Request().InputBool("force") {
return Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在")
}
if err := io.Mv(request.Source, request.Target); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Target, 0755, "www", "www")
return Success(ctx, nil)
}
// Copy
//
// @Summary 复制文件/目录
// @Description 复制文件/目录到给定路径
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Copy true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/copy [post]
func (r *FileController) Copy(ctx http.Context) http.Response {
var request requests.Copy
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
if io.Exists(request.Target) && !ctx.Request().InputBool("force") {
return Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在")
}
if err := io.Cp(request.Source, request.Target); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Source, 0755, "www", "www")
return Success(ctx, nil)
}
// Download
//
// @Summary 下载文件
// @Description 下载给定路径的文件
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data query requests.NotExist true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/download [get]
func (r *FileController) Download(ctx http.Context) http.Response {
var request requests.Exist
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
info, err := io.FileInfo(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if info.IsDir() {
return Error(ctx, http.StatusInternalServerError, "不能下载目录")
}
return ctx.Response().Download(request.Path, info.Name())
}
// RemoteDownload
//
// @Summary 下载远程文件
// @Description 下载远程文件到给定路径
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.NotExist true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/remoteDownload [post]
func (r *FileController) RemoteDownload(ctx http.Context) http.Response {
var request requests.NotExist
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
// TODO 使用异步任务下载文件
return nil
}
// Info
//
// @Summary 获取文件/目录信息
// @Description 获取给定路径的文件/目录信息
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data query requests.Exist true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/info [get]
func (r *FileController) Info(ctx http.Context) http.Response {
var request requests.Exist
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
fileInfo, err := io.FileInfo(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, http.Json{
"name": fileInfo.Name(),
"size": str.FormatBytes(float64(fileInfo.Size())),
"mode_str": fileInfo.Mode().String(),
"mode": fmt.Sprintf("%04o", fileInfo.Mode().Perm()),
"dir": fileInfo.IsDir(),
"modify": carbon.FromStdTime(fileInfo.ModTime()).ToDateTimeString(),
})
}
// Permission
//
// @Summary 修改文件/目录权限
// @Description 修改给定路径的文件/目录权限
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Permission true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/permission [post]
func (r *FileController) Permission(ctx http.Context) http.Response {
var request requests.Permission
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
// 解析成8进制
mode, err := strconv.ParseUint(request.Mode, 8, 64)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = io.Chmod(request.Path, stdos.FileMode(mode)); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = io.Chown(request.Path, request.Owner, request.Group); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
// Archive
//
// @Summary 压缩文件/目录
// @Description 压缩文件/目录到给定路径
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Archive true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/archive [post]
func (r *FileController) Archive(ctx http.Context) http.Response {
var request requests.Archive
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
if err := io.Archive(request.Paths, request.File); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.File, 0755, "www", "www")
return Success(ctx, nil)
}
// UnArchive
//
// @Summary 解压文件/目录
// @Description 解压文件/目录到给定路径
// @Tags 文件
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.UnArchive true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/unArchive [post]
func (r *FileController) UnArchive(ctx http.Context) http.Response {
var request requests.UnArchive
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
if err := io.UnArchive(request.File, request.Path); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Path, 0755, "www", "www")
return Success(ctx, nil)
}
// Search
//
// @Summary 搜索文件/目录
// @Description 通过关键词搜索给定路径的文件/目录
// @Tags 文件
// @Accept json
// @Produce json
// @Param data body requests.Search true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/search [post]
func (r *FileController) Search(ctx http.Context) http.Response {
var request requests.Search
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
paths := make(map[string]stdos.FileInfo)
err := filepath.Walk(request.Path, func(path string, info stdos.FileInfo, err error) error {
if err != nil {
return err
}
if strings.Contains(info.Name(), request.KeyWord) {
paths[path] = info
}
return nil
})
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, paths)
}
// List
//
// @Summary 获取文件/目录列表
// @Description 获取给定路径的文件/目录列表
// @Tags 文件
// @Accept json
// @Produce json
// @Param data query requests.Exist true "request"
// @Param data query commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/list [get]
func (r *FileController) List(ctx http.Context) http.Response {
var request requests.Exist
sanitize := SanitizeRequest(ctx, &request)
if sanitize != nil {
return sanitize
}
fileInfoList, err := io.ReadDir(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
var paths []any
for _, fileInfo := range fileInfoList {
info, _ := fileInfo.Info()
stat := info.Sys().(*syscall.Stat_t)
paths = append(paths, map[string]any{
"name": info.Name(),
"full": filepath.Join(request.Path, info.Name()),
"size": str.FormatBytes(float64(info.Size())),
"mode_str": info.Mode().String(),
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
"owner": os.GetUser(stat.Uid),
"group": os.GetGroup(stat.Gid),
"uid": stat.Uid,
"gid": stat.Gid,
"hidden": io.IsHidden(info.Name()),
"symlink": io.IsSymlink(info.Mode()),
"link": io.GetSymlink(filepath.Join(request.Path, info.Name())),
"dir": info.IsDir(),
"modify": carbon.FromStdTime(info.ModTime()).ToDateTimeString(),
})
}
paged, total := Paginate(ctx, paths)
return Success(ctx, http.Json{
"total": total,
"items": paged,
})
}
// setPermission
func (r *FileController) setPermission(path string, mode stdos.FileMode, owner, group string) {
_ = io.Chmod(path, mode)
_ = io.Chown(path, owner, group)
}
+53 -26
View File
@@ -8,10 +8,15 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/hashicorp/go-version"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/tools"
"github.com/TheTNB/panel/pkg/types"
)
type MenuItem struct {
@@ -22,8 +27,8 @@ type MenuItem struct {
}
type InfoController struct {
plugin services.Plugin
setting services.Setting
plugin internal.Plugin
setting internal.Setting
}
func NewInfoController() *InfoController {
@@ -33,10 +38,11 @@ func NewInfoController() *InfoController {
}
}
// Name 获取面板名称
func (r *InfoController) Name(ctx http.Context) http.Response {
// Panel 获取面板信息
func (r *InfoController) Panel(ctx http.Context) http.Response {
return Success(ctx, http.Json{
"name": r.setting.Get(models.SettingKeyName),
"name": r.setting.Get(models.SettingKeyName),
"language": facades.Config().GetString("app.locale"),
})
}
@@ -60,7 +66,7 @@ func (r *InfoController) HomePlugins(ctx http.Context) http.Response {
for _, plugin := range plugins {
pluginsJson = append(pluginsJson, pluginsData{
Plugin: plugin,
Name: services.NewPluginImpl().GetBySlug(plugin.Slug).Name,
Name: r.plugin.GetBySlug(plugin.Slug).Name,
})
}
@@ -105,8 +111,8 @@ func (r *InfoController) CountInfo(ctx http.Context) http.Response {
}
var databaseCount int64
if mysqlInstalled {
status, err := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
if status == "active" && err == nil {
status, err := systemctl.Status("mysqld")
if status && err == nil {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
type database struct {
Name string `json:"name"`
@@ -147,9 +153,9 @@ func (r *InfoController) CountInfo(ctx http.Context) http.Response {
}
}
if postgresqlInstalled {
status, err := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
if status == "active" && err == nil {
raw, err := tools.Exec(`echo "\l" | su - postgres -c "psql"`)
status, err := systemctl.Status("postgresql")
if status && err == nil {
raw, err := shell.Execf(`echo "\l" | su - postgres -c "psql"`)
if err == nil {
databases := strings.Split(raw, "\n")
if len(databases) >= 4 {
@@ -170,7 +176,7 @@ func (r *InfoController) CountInfo(ctx http.Context) http.Response {
var ftpCount int64
var ftpPlugin = r.plugin.GetInstalledBySlug("pureftpd")
if ftpPlugin.ID != 0 {
listRaw, err := tools.Exec("pure-pw list")
listRaw, err := shell.Execf("pure-pw list")
if len(listRaw) != 0 && err == nil {
listArr := strings.Split(listRaw, "\n")
ftpCount = int64(len(listArr))
@@ -245,13 +251,21 @@ func (r *InfoController) InstalledDbAndPhp(ctx http.Context) http.Response {
// CheckUpdate 检查面板更新
func (r *InfoController) CheckUpdate(ctx http.Context) http.Response {
version := facades.Config().GetString("panel.version")
remote, err := tools.GetLatestPanelVersion()
current := facades.Config().GetString("panel.version")
latest, err := tools.GetLatestPanelVersion()
if err != nil {
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
}
if tools.VersionCompare(version, remote.Version, ">=") {
v1, err := version.NewVersion(current)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "版本号解析失败")
}
v2, err := version.NewVersion(latest.Version)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "版本号解析失败")
}
if v1.GreaterThanOrEqual(v2) {
return Success(ctx, http.Json{
"update": false,
})
@@ -264,17 +278,25 @@ func (r *InfoController) CheckUpdate(ctx http.Context) http.Response {
// UpdateInfo 获取更新信息
func (r *InfoController) UpdateInfo(ctx http.Context) http.Response {
version := facades.Config().GetString("panel.version")
current, err := tools.GetLatestPanelVersion()
current := facades.Config().GetString("panel.version")
latest, err := tools.GetLatestPanelVersion()
if err != nil {
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
}
if tools.VersionCompare(version, current.Version, ">=") {
v1, err := version.NewVersion(current)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "版本号解析失败")
}
v2, err := version.NewVersion(latest.Version)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "版本号解析失败")
}
if v1.GreaterThanOrEqual(v2) {
return Error(ctx, http.StatusInternalServerError, "当前版本已是最新版本")
}
versions, err := tools.GenerateVersions(version, current.Version)
versions, err := tools.GenerateVersions(current, latest.Version)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "获取更新信息失败")
}
@@ -295,10 +317,13 @@ func (r *InfoController) UpdateInfo(ctx http.Context) http.Response {
// Update 更新面板
func (r *InfoController) Update(ctx http.Context) http.Response {
var task models.Task
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
if err == nil {
if err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task); err == nil {
return Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止更新")
}
if _, err := facades.Orm().Query().Exec("PRAGMA wal_checkpoint(TRUNCATE)"); err != nil {
types.Status = types.StatusFailed
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("面板数据库异常,已终止操作:%s", err.Error()))
}
panel, err := tools.GetLatestPanelVersion()
if err != nil {
@@ -308,14 +333,16 @@ func (r *InfoController) Update(ctx http.Context) http.Response {
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
}
err = tools.UpdatePanel(panel)
if err != nil {
types.Status = types.StatusUpgrade
if err = tools.UpdatePanel(panel); err != nil {
types.Status = types.StatusFailed
facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{
"error": err.Error(),
}).Info("更新面板失败")
return Error(ctx, http.StatusInternalServerError, err.Error())
}
types.Status = types.StatusNormal
tools.RestartPanel()
return Success(ctx, nil)
}
+4 -3
View File
@@ -8,12 +8,13 @@ import (
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
)
type MonitorController struct {
setting services.Setting
setting internal.Setting
}
func NewMonitorController() *MonitorController {
+89 -189
View File
@@ -1,18 +1,17 @@
package controllers
import (
"sync"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/app/services"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
)
type PluginController struct {
plugin services.Plugin
task services.Task
plugin internal.Plugin
task internal.Task
}
func NewPluginController() *PluginController {
@@ -22,7 +21,14 @@ func NewPluginController() *PluginController {
}
}
// List 列出所有插件
// List
//
// @Summary 插件列表
// @Tags 插件
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/plugin/list [get]
func (r *PluginController) List(ctx http.Context) http.Response {
plugins := r.plugin.All()
installedPlugins, err := r.plugin.AllInstalled()
@@ -30,13 +36,10 @@ func (r *PluginController) List(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
var lock sync.RWMutex
installedPluginsMap := make(map[string]models.Plugin)
for _, p := range installedPlugins {
lock.Lock()
installedPluginsMap[p.Slug] = p
lock.Unlock()
}
type plugin struct {
@@ -72,211 +75,81 @@ func (r *PluginController) List(ctx http.Context) http.Response {
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(pluginArr) {
return Success(ctx, http.Json{
"total": 0,
"items": []plugin{},
})
}
if endIndex > len(pluginArr) {
endIndex = len(pluginArr)
}
pagedPlugins := pluginArr[startIndex:endIndex]
paged, total := Paginate(ctx, pluginArr)
return Success(ctx, http.Json{
"total": len(pluginArr),
"items": pagedPlugins,
"total": total,
"items": paged,
})
}
// Install 安装插件
// Install
//
// @Summary 安装插件
// @Tags 插件
// @Produce json
// @Security BearerToken
// @Param slug query string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/plugin/install [post]
func (r *PluginController) Install(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
plugin := r.plugin.GetBySlug(slug)
installedPlugin := r.plugin.GetInstalledBySlug(slug)
installedPlugins, err := r.plugin.AllInstalled()
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
"slug": slug,
}).Info("检查插件安装状态失败")
return ErrorSystem(ctx)
if err := r.plugin.Install(slug); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if installedPlugin.ID != 0 {
return Error(ctx, http.StatusUnprocessableEntity, "插件已安装")
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
}
}
var task models.Task
task.Name = "安装插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
task.Shell = plugin.Install + " >> /tmp/" + plugin.Slug + ".log 2>&1"
task.Log = "/tmp/" + plugin.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
"slug": slug,
"err": err.Error(),
}).Info("创建任务失败")
return ErrorSystem(ctx)
}
r.task.Process(task.ID)
return Success(ctx, "任务已提交")
}
// Uninstall 卸载插件
// Uninstall
//
// @Summary 卸载插件
// @Tags 插件
// @Produce json
// @Security BearerToken
// @Param slug query string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/plugin/uninstall [post]
func (r *PluginController) Uninstall(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
plugin := r.plugin.GetBySlug(slug)
installedPlugin := r.plugin.GetInstalledBySlug(slug)
installedPlugins, err := r.plugin.AllInstalled()
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
"slug": slug,
}).Info("检查插件安装状态失败")
return ErrorSystem(ctx)
if err := r.plugin.Uninstall(slug); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if installedPlugin.ID == 0 {
return Error(ctx, http.StatusUnprocessableEntity, "插件未安装")
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
}
}
var task models.Task
task.Name = "卸载插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
task.Shell = plugin.Uninstall + " >> /tmp/" + plugin.Slug + ".log 2>&1"
task.Log = "/tmp/" + plugin.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
"slug": slug,
"err": err.Error(),
}).Info("创建任务失败")
return ErrorSystem(ctx)
}
r.task.Process(task.ID)
return Success(ctx, "任务已提交")
}
// Update 更新插件
// Update
//
// @Summary 更新插件
// @Tags 插件
// @Produce json
// @Security BearerToken
// @Param slug query string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/plugin/update [post]
func (r *PluginController) Update(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
plugin := r.plugin.GetBySlug(slug)
installedPlugin := r.plugin.GetInstalledBySlug(slug)
installedPlugins, err := r.plugin.AllInstalled()
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
"slug": slug,
}).Info("检查插件安装状态失败")
return ErrorSystem(ctx)
if err := r.plugin.Update(slug); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if installedPlugin.ID == 0 {
return Error(ctx, http.StatusUnprocessableEntity, "插件未安装")
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
}
}
var task models.Task
task.Name = "更新插件 " + plugin.Name
task.Status = models.TaskStatusWaiting
task.Shell = plugin.Update + " >> /tmp/" + plugin.Slug + ".log 2>&1"
task.Log = "/tmp/" + plugin.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "插件中心").With(map[string]any{
"slug": slug,
"err": err.Error(),
}).Info("创建任务失败")
return ErrorSystem(ctx)
}
r.task.Process(task.ID)
return Success(ctx, "任务已提交")
}
// UpdateShow 更新插件首页显示状态
// UpdateShow
//
// @Summary 更新插件首页显示状态
// @Tags 插件
// @Produce json
// @Security BearerToken
// @Param slug query string true "request"
// @Param show query bool true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/plugin/updateShow [post]
func (r *PluginController) UpdateShow(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
show := ctx.Request().InputBool("show")
@@ -304,3 +177,30 @@ func (r *PluginController) UpdateShow(ctx http.Context) http.Response {
return Success(ctx, "操作成功")
}
// IsInstalled
//
// @Summary 检查插件是否已安装
// @Tags 插件
// @Produce json
// @Security BearerToken
// @Param slug query string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/plugin/isInstalled [get]
func (r *PluginController) IsInstalled(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
plugin := r.plugin.GetInstalledBySlug(slug)
info := r.plugin.GetBySlug(slug)
if plugin.Slug != slug {
return Success(ctx, http.Json{
"name": info.Name,
"installed": false,
})
}
return Success(ctx, http.Json{
"name": info.Name,
"installed": true,
})
}
-29
View File
@@ -1,29 +0,0 @@
package plugins
type PHPExtension struct {
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
Installed bool `json:"installed"`
}
type LoadInfo struct {
Name string `json:"name"`
Value string `json:"value"`
}
type Fail2banJail struct {
Name string `json:"name"`
Enabled bool `json:"enabled"`
LogPath string `json:"log_path"`
MaxRetry int `json:"max_retry"`
FindTime int `json:"find_time"`
BanTime int `json:"ban_time"`
}
type S3fsMount struct {
ID int64 `json:"id"`
Path string `json:"path"`
Bucket string `json:"bucket"`
Url string `json:"url"`
}
@@ -8,14 +8,19 @@ import (
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"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/types"
)
type Fail2banController struct {
website services.Website
website internal.Website
}
func NewFail2banController() *Fail2banController {
@@ -24,61 +29,9 @@ func NewFail2banController() *Fail2banController {
}
}
// Status 获取运行状态
func (r *Fail2banController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("fail2ban")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
}
return controllers.Success(ctx, status)
}
// Reload 重载配置
func (r *Fail2banController) Reload(ctx http.Context) http.Response {
if _, err := tools.Exec("systemctl reload fail2ban"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载配置失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启服务
func (r *Fail2banController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("fail2ban"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启服务失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *Fail2banController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("fail2ban"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动服务失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *Fail2banController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("fail2ban"); err != nil {
return nil
}
status, err := tools.ServiceStatus("fail2ban")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
}
return controllers.Success(ctx, !status)
}
// List 所有 Fail2ban 规则
func (r *Fail2banController) List(ctx http.Context) http.Response {
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
raw, err := tools.Read("/etc/fail2ban/jail.local")
raw, err := io.Read("/etc/fail2ban/jail.local")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
@@ -88,14 +41,14 @@ func (r *Fail2banController) List(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "Fail2ban 规则为空")
}
var jails []Fail2banJail
var jails []types.Fail2banJail
for i, jail := range jailList {
if i == 0 {
continue
}
jailName := jail[1]
jailRaw := tools.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
jailRaw := str.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
if len(jailRaw) == 0 {
continue
}
@@ -105,7 +58,7 @@ func (r *Fail2banController) List(ctx http.Context) http.Response {
jailFindTime := regexp.MustCompile(`findtime = (.*)`).FindStringSubmatch(jailRaw)
jailBanTime := regexp.MustCompile(`bantime = (.*)`).FindStringSubmatch(jailRaw)
jails = append(jails, Fail2banJail{
jails = append(jails, types.Fail2banJail{
Name: jailName,
Enabled: jailEnabled,
LogPath: jailLogPath[1],
@@ -115,31 +68,17 @@ func (r *Fail2banController) List(ctx http.Context) http.Response {
})
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(jails) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []Fail2banJail{},
})
}
if endIndex > len(jails) {
endIndex = len(jails)
}
pagedJails := jails[startIndex:endIndex]
if pagedJails == nil {
pagedJails = []Fail2banJail{}
}
paged, total := controllers.Paginate(ctx, jails)
return controllers.Success(ctx, http.Json{
"total": len(jails),
"items": pagedJails,
"total": total,
"items": paged,
})
}
// Add 添加 Fail2ban 规则
func (r *Fail2banController) Add(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
if sanitize := controllers.Sanitize(ctx, map[string]string{
"name": "required",
"type": "required|in:website,service",
"maxretry": "required",
@@ -148,12 +87,8 @@ func (r *Fail2banController) Add(ctx http.Context) http.Response {
"website_name": "required_if:type,website",
"website_mode": "required_if:type,website",
"website_path": "required_if:website_mode,path",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}); sanitize != nil {
return sanitize
}
jailName := ctx.Request().Input("name")
@@ -165,7 +100,7 @@ func (r *Fail2banController) Add(ctx http.Context) http.Response {
jailWebsiteMode := ctx.Request().Input("website_mode")
jailWebsitePath := ctx.Request().Input("website_path")
raw, err := tools.Read("/etc/fail2ban/jail.local")
raw, err := io.Read("/etc/fail2ban/jail.local")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
@@ -204,7 +139,7 @@ logpath = /www/wwwlogs/` + website.Name + `.log
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-END
`
raw += rule
if err = tools.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
}
@@ -222,7 +157,7 @@ failregex = ^<HOST>\s-.*\s` + jailWebsitePath + `.*HTTP/.*$
ignoreregex =
`
}
if err = tools.Write("/etc/fail2ban/filter.d/haozi-"+jailWebsiteName+"-"+jailWebsiteMode+".conf", filter, 0644); err != nil {
if err = io.Write("/etc/fail2ban/filter.d/haozi-"+jailWebsiteName+"-"+jailWebsiteMode+".conf", filter, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
}
@@ -233,21 +168,21 @@ ignoreregex =
var err error
switch jailName {
case "ssh":
if tools.IsDebian() {
if os.IsDebian() {
logPath = "/var/log/auth.log"
} else {
logPath = "/var/log/secure"
}
filter = "sshd"
port, err = tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
case "mysql":
logPath = "/www/server/mysql/mysql-error.log"
filter = "mysqld-auth"
port, err = tools.Exec("cat /www/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'")
port, err = shell.Execf("cat /www/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'")
case "pure-ftpd":
logPath = "/var/log/messages"
filter = "pure-ftpd"
port, err = tools.Exec(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
port, err = shell.Execf(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
default:
return controllers.Error(ctx, http.StatusUnprocessableEntity, "未知服务")
}
@@ -269,12 +204,12 @@ logpath = ` + logPath + `
# ` + jailName + `-END
`
raw += rule
if err := tools.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
}
}
if _, err := tools.Exec("fail2ban-client reload"); err != nil {
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载配置失败")
}
@@ -284,7 +219,7 @@ logpath = ` + logPath + `
// Delete 删除规则
func (r *Fail2banController) Delete(ctx http.Context) http.Response {
jailName := ctx.Request().Input("name")
raw, err := tools.Read("/etc/fail2ban/jail.local")
raw, err := io.Read("/etc/fail2ban/jail.local")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
@@ -292,14 +227,14 @@ func (r *Fail2banController) Delete(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "规则不存在")
}
rule := tools.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
rule := str.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
raw = strings.Replace(raw, "\n# "+jailName+"-START"+rule+"# "+jailName+"-END", "", -1)
raw = strings.TrimSpace(raw)
if err := tools.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
}
if _, err := tools.Exec("fail2ban-client reload"); err != nil {
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载配置失败")
}
@@ -313,15 +248,15 @@ func (r *Fail2banController) BanList(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
}
currentlyBan, err := tools.Exec(`fail2ban-client status ` + name + ` | grep "Currently banned" | awk '{print $4}'`)
currentlyBan, err := shell.Execf(`fail2ban-client status %s | grep "Currently banned" | awk '{print $4}'`, name)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败")
}
totalBan, err := tools.Exec(`fail2ban-client status ` + name + ` | grep "Total banned" | awk '{print $4}'`)
totalBan, err := shell.Execf(`fail2ban-client status %s | grep "Total banned" | awk '{print $4}'`, name)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败")
}
bannedIp, err := tools.Exec(`fail2ban-client status ` + name + ` | grep "Banned IP list" | awk -F ":" '{print $2}'`)
bannedIp, err := shell.Execf(`fail2ban-client status %s | grep "Banned IP list" | awk -F ":" '{print $2}'`, name)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取封禁列表失败")
}
@@ -355,7 +290,7 @@ func (r *Fail2banController) Unban(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
}
if _, err := tools.Exec("fail2ban-client set " + name + " unbanip " + ip); err != nil {
if _, err := shell.Execf("fail2ban-client set %s unbanip %s", name, ip); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "解封失败")
}
@@ -369,7 +304,7 @@ func (r *Fail2banController) SetWhiteList(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
}
raw, err := tools.Read("/etc/fail2ban/jail.local")
raw, err := io.Read("/etc/fail2ban/jail.local")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
@@ -381,11 +316,11 @@ func (r *Fail2banController) SetWhiteList(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
}
if err := tools.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Fail2ban规则失败")
}
if _, err := tools.Exec("fail2ban-client reload"); err != nil {
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载配置失败")
}
return controllers.Success(ctx, nil)
@@ -393,7 +328,7 @@ func (r *Fail2banController) SetWhiteList(ctx http.Context) http.Response {
// GetWhiteList 获取白名单
func (r *Fail2banController) GetWhiteList(ctx http.Context) http.Response {
raw, err := tools.Read("/etc/fail2ban/jail.local")
raw, err := io.Read("/etc/fail2ban/jail.local")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
@@ -0,0 +1,72 @@
package plugins
import (
"fmt"
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/app/http/controllers"
requests "github.com/TheTNB/panel/app/http/requests/plugins/frp"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/systemctl"
)
type FrpController struct {
}
func NewFrpController() *FrpController {
return &FrpController{}
}
// GetConfig
//
// @Summary 获取配置
// @Description 获取 Frp 配置
// @Tags 插件-Frp
// @Produce json
// @Security BearerToken
// @Param service query string false "服务"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/frp/config [get]
func (r *FrpController) GetConfig(ctx http.Context) http.Response {
var serviceRequest requests.Service
sanitize := controllers.SanitizeRequest(ctx, &serviceRequest)
if sanitize != nil {
return sanitize
}
config, err := io.Read(fmt.Sprintf("/www/server/frp/%s.toml", serviceRequest.Service))
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
// UpdateConfig
//
// @Summary 更新配置
// @Description 更新 Frp 配置
// @Tags 插件-Frp
// @Produce json
// @Security BearerToken
// @Param data body requests.UpdateConfig true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/frp/config [post]
func (r *FrpController) UpdateConfig(ctx http.Context) http.Response {
var updateRequest requests.UpdateConfig
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
if err := io.Write(fmt.Sprintf("/www/server/frp/%s.toml", updateRequest.Service), updateRequest.Config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := systemctl.Restart(updateRequest.Service); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
@@ -0,0 +1,63 @@
package plugins
import (
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/app/http/controllers"
requests "github.com/TheTNB/panel/app/http/requests/plugins/gitea"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/systemctl"
)
type GiteaController struct {
}
func NewGiteaController() *GiteaController {
return &GiteaController{}
}
// GetConfig
//
// @Summary 获取配置
// @Description 获取 Gitea 配置
// @Tags 插件-Gitea
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/gitea/config [get]
func (r *GiteaController) GetConfig(ctx http.Context) http.Response {
config, err := io.Read("/www/server/gitea/app.ini")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
// UpdateConfig
//
// @Summary 更新配置
// @Description 更新 Gitea 配置
// @Tags 插件-Gitea
// @Produce json
// @Security BearerToken
// @Param data body requests.UpdateConfig true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/gitea/config [post]
func (r *GiteaController) UpdateConfig(ctx http.Context) http.Response {
var updateRequest requests.UpdateConfig
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
if err := io.Write("/www/server/gitea/app.ini", updateRequest.Config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := systemctl.Restart("gitea"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
@@ -1,666 +0,0 @@
package plugins
import (
"database/sql"
"fmt"
"regexp"
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Mysql57Controller struct {
setting services.Setting
backup services.Backup
}
func NewMysql57Controller() *Mysql57Controller {
return &Mysql57Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// Status 获取运行状态
func (r *Mysql57Controller) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
}
return controllers.Success(ctx, status)
}
// Reload 重载配置
func (r *Mysql57Controller) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载MySQL失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启服务
func (r *Mysql57Controller) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启MySQL服务失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *Mysql57Controller) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动MySQL服务失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *Mysql57Controller) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止MySQL服务失败")
}
return controllers.Success(ctx, nil)
}
// GetConfig 获取配置
func (r *Mysql57Controller) GetConfig(ctx http.Context) http.Response {
config, err := tools.Read("/www/server/mysql/conf/my.cnf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *Mysql57Controller) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/mysql/conf/my.cnf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
}
return r.Restart(ctx)
}
// Load 获取负载
func (r *Mysql57Controller) Load(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
}
status, err := tools.ServiceStatus("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
}
raw, err := tools.Exec("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
}
var data []map[string]string
expressions := []struct {
regex string
name string
}{
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
}
for _, expression := range expressions {
re := regexp.MustCompile(expression.regex)
matches := re.FindStringSubmatch(raw)
if len(matches) > 1 {
d := map[string]string{"name": expression.name, "value": matches[1]}
if expression.name == "发送" || expression.name == "接收" {
d["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
}
data = append(data, d)
}
}
// 索引命中率
readRequests := cast.ToFloat64(data[9]["value"])
reads := cast.ToFloat64(data[10]["value"])
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
// Innodb 索引命中率
bufferPoolReads := cast.ToFloat64(data[11]["value"])
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
return controllers.Success(ctx, data)
}
// ErrorLog 获取错误日志
func (r *Mysql57Controller) ErrorLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/mysql/mysql-error.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearErrorLog 清空错误日志
func (r *Mysql57Controller) ClearErrorLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/mysql/mysql-error.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// SlowLog 获取慢查询日志
func (r *Mysql57Controller) SlowLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/mysql/mysql-slow.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearSlowLog 清空慢查询日志
func (r *Mysql57Controller) ClearSlowLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/mysql/mysql-slow.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// GetRootPassword 获取root密码
func (r *Mysql57Controller) GetRootPassword(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
}
return controllers.Success(ctx, rootPassword)
}
// SetRootPassword 设置root密码
func (r *Mysql57Controller) SetRootPassword(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
}
rootPassword := ctx.Request().Input("password")
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码不能为空")
}
oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if oldRootPassword != rootPassword {
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
}
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
}
err := r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
if err != nil {
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\""); err != nil {
return nil
}
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return nil
}
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
}
}
return controllers.Success(ctx, nil)
}
// DatabaseList 获取数据库列表
func (r *Mysql57Controller) DatabaseList(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
type database struct {
Name string `json:"name"`
}
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer db.Close()
rows, err := db.Query("SHOW DATABASES")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var databases []database
for rows.Next() {
var d database
err := rows.Scan(&d.Name)
if err != nil {
continue
}
databases = append(databases, d)
}
if err := rows.Err(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(databases) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
if endIndex > len(databases) {
endIndex = len(databases)
}
pagedDatabases := databases[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(databases),
"items": pagedDatabases,
})
}
// AddDatabase 添加数据库
func (r *Mysql57Controller) AddDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// DeleteDatabase 删除数据库
func (r *Mysql57Controller) DeleteDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// BackupList 获取备份列表
func (r *Mysql57Controller) BackupList(ctx http.Context) http.Response {
backupList, err := r.backup.MysqlList()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(backupList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []services.BackupFile{},
})
}
if endIndex > len(backupList) {
endIndex = len(backupList)
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []services.BackupFile{}
}
return controllers.Success(ctx, http.Json{
"total": len(backupList),
"items": pagedBackupList,
})
}
// UploadBackup 上传备份
func (r *Mysql57Controller) UploadBackup(ctx http.Context) http.Response {
file, err := ctx.Request().File("file")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
if err = tools.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
return controllers.Success(ctx, nil)
}
// CreateBackup 创建备份
func (r *Mysql57Controller) CreateBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
err = r.backup.MysqlBackup(database)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// DeleteBackup 删除备份
func (r *Mysql57Controller) DeleteBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
fileName := ctx.Request().Input("name")
if err := tools.Remove(backupPath + "/" + fileName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// RestoreBackup 还原备份
func (r *Mysql57Controller) RestoreBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"backup": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
err = r.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup"))
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// UserList 用户列表
func (r *Mysql57Controller) UserList(ctx http.Context) http.Response {
type user struct {
User string `json:"user"`
Host string `json:"host"`
Grants []string `json:"grants"`
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer db.Close()
rows, err := db.Query("SELECT user, host FROM mysql.user")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var userGrants []user
for rows.Next() {
var u user
err := rows.Scan(&u.User, &u.Host)
if err != nil {
continue
}
// 查询用户权限
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
if err != nil {
continue
}
defer grantsRows.Close()
for grantsRows.Next() {
var grant string
err := grantsRows.Scan(&grant)
if err != nil {
continue
}
u.Grants = append(u.Grants, grant)
}
if err := grantsRows.Err(); err != nil {
continue
}
userGrants = append(userGrants, u)
}
if err := rows.Err(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(userGrants) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
})
}
if endIndex > len(userGrants) {
endIndex = len(userGrants)
}
pagedUserGrants := userGrants[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(userGrants),
"items": pagedUserGrants,
})
}
// AddUser 添加用户
func (r *Mysql57Controller) AddUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// DeleteUser 删除用户
func (r *Mysql57Controller) DeleteUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// SetUserPassword 设置用户密码
func (r *Mysql57Controller) SetUserPassword(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// SetUserPrivileges 设置用户权限
func (r *Mysql57Controller) SetUserPrivileges(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"database": "required|min_len:1|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
@@ -1,666 +0,0 @@
package plugins
import (
"database/sql"
"fmt"
"regexp"
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Mysql80Controller struct {
setting services.Setting
backup services.Backup
}
func NewMysql80Controller() *Mysql80Controller {
return &Mysql80Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// Status 获取运行状态
func (r *Mysql80Controller) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
}
return controllers.Success(ctx, status)
}
// Reload 重载配置
func (r *Mysql80Controller) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载MySQL配置失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启服务
func (r *Mysql80Controller) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启MySQL服务失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *Mysql80Controller) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动MySQL服务失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *Mysql80Controller) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止MySQL服务失败")
}
return controllers.Success(ctx, nil)
}
// GetConfig 获取配置
func (r *Mysql80Controller) GetConfig(ctx http.Context) http.Response {
config, err := tools.Read("/www/server/mysql/conf/my.cnf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *Mysql80Controller) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/mysql/conf/my.cnf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
}
return r.Restart(ctx)
}
// Load 获取负载
func (r *Mysql80Controller) Load(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
}
status, err := tools.ServiceStatus("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
}
raw, err := tools.Exec("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
}
var data []map[string]string
expressions := []struct {
regex string
name string
}{
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
}
for _, expression := range expressions {
re := regexp.MustCompile(expression.regex)
matches := re.FindStringSubmatch(raw)
if len(matches) > 1 {
d := map[string]string{"name": expression.name, "value": matches[1]}
if expression.name == "发送" || expression.name == "接收" {
d["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
}
data = append(data, d)
}
}
// 索引命中率
readRequests := cast.ToFloat64(data[9]["value"])
reads := cast.ToFloat64(data[10]["value"])
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
// Innodb 索引命中率
bufferPoolReads := cast.ToFloat64(data[11]["value"])
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
return controllers.Success(ctx, data)
}
// ErrorLog 获取错误日志
func (r *Mysql80Controller) ErrorLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/mysql/mysql-error.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearErrorLog 清空错误日志
func (r *Mysql80Controller) ClearErrorLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/mysql/mysql-error.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// SlowLog 获取慢查询日志
func (r *Mysql80Controller) SlowLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/mysql/mysql-slow.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearSlowLog 清空慢查询日志
func (r *Mysql80Controller) ClearSlowLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/mysql/mysql-slow.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// GetRootPassword 获取root密码
func (r *Mysql80Controller) GetRootPassword(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
}
return controllers.Success(ctx, rootPassword)
}
// SetRootPassword 设置root密码
func (r *Mysql80Controller) SetRootPassword(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
}
rootPassword := ctx.Request().Input("password")
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码不能为空")
}
oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if oldRootPassword != rootPassword {
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
}
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
}
err := r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
if err != nil {
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\""); err != nil {
return nil
}
if _, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return nil
}
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
}
}
return controllers.Success(ctx, nil)
}
// DatabaseList 获取数据库列表
func (r *Mysql80Controller) DatabaseList(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
type database struct {
Name string `json:"name"`
}
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer db.Close()
rows, err := db.Query("SHOW DATABASES")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var databases []database
for rows.Next() {
var d database
err := rows.Scan(&d.Name)
if err != nil {
continue
}
databases = append(databases, d)
}
if err := rows.Err(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(databases) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
if endIndex > len(databases) {
endIndex = len(databases)
}
pagedDatabases := databases[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(databases),
"items": pagedDatabases,
})
}
// AddDatabase 添加数据库
func (r *Mysql80Controller) AddDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// DeleteDatabase 删除数据库
func (r *Mysql80Controller) DeleteDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// BackupList 获取备份列表
func (r *Mysql80Controller) BackupList(ctx http.Context) http.Response {
backupList, err := r.backup.MysqlList()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(backupList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []services.BackupFile{},
})
}
if endIndex > len(backupList) {
endIndex = len(backupList)
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []services.BackupFile{}
}
return controllers.Success(ctx, http.Json{
"total": len(backupList),
"items": pagedBackupList,
})
}
// UploadBackup 上传备份
func (r *Mysql80Controller) UploadBackup(ctx http.Context) http.Response {
file, err := ctx.Request().File("file")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !tools.Exists(backupPath) {
if err = tools.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
return controllers.Success(ctx, nil)
}
// CreateBackup 创建备份
func (r *Mysql80Controller) CreateBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
err = r.backup.MysqlBackup(database)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// DeleteBackup 删除备份
func (r *Mysql80Controller) DeleteBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
fileName := ctx.Request().Input("name")
if err := tools.Remove(backupPath + "/" + fileName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// RestoreBackup 还原备份
func (r *Mysql80Controller) RestoreBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"backup": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
err = r.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup"))
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// UserList 用户列表
func (r *Mysql80Controller) UserList(ctx http.Context) http.Response {
type user struct {
User string `json:"user"`
Host string `json:"host"`
Grants []string `json:"grants"`
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer db.Close()
rows, err := db.Query("SELECT user, host FROM mysql.user")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var userGrants []user
for rows.Next() {
var u user
err := rows.Scan(&u.User, &u.Host)
if err != nil {
continue
}
// 查询用户权限
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
if err != nil {
continue
}
defer grantsRows.Close()
for grantsRows.Next() {
var grant string
err := grantsRows.Scan(&grant)
if err != nil {
continue
}
u.Grants = append(u.Grants, grant)
}
if err := grantsRows.Err(); err != nil {
continue
}
userGrants = append(userGrants, u)
}
if err := rows.Err(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(userGrants) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
})
}
if endIndex > len(userGrants) {
endIndex = len(userGrants)
}
pagedUserGrants := userGrants[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(userGrants),
"items": pagedUserGrants,
})
}
// AddUser 添加用户
func (r *Mysql80Controller) AddUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// DeleteUser 删除用户
func (r *Mysql80Controller) DeleteUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// SetUserPassword 设置用户密码
func (r *Mysql80Controller) SetUserPassword(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// SetUserPrivileges 设置用户权限
func (r *Mysql80Controller) SetUserPrivileges(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"database": "required|min_len:1|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\""); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
@@ -0,0 +1,506 @@
package plugins
import (
"fmt"
"regexp"
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"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/types"
)
type MySQLController struct {
setting internal.Setting
backup internal.Backup
}
func NewMySQLController() *MySQLController {
return &MySQLController{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// GetConfig 获取配置
func (r *MySQLController) GetConfig(ctx http.Context) http.Response {
config, err := io.Read("/www/server/mysql/conf/my.cnf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *MySQLController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := io.Write("/www/server/mysql/conf/my.cnf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
}
if err := systemctl.Reload("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载MySQL失败")
}
return controllers.Success(ctx, nil)
}
// Load 获取负载
func (r *MySQLController) Load(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
}
status, _ := systemctl.Status("mysqld")
if !status {
return controllers.Success(ctx, []types.NV{})
}
raw, err := shell.Execf("mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
}
var data []map[string]string
expressions := []struct {
regex string
name string
}{
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
}
for _, expression := range expressions {
re := regexp.MustCompile(expression.regex)
matches := re.FindStringSubmatch(raw)
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]))
}
data = append(data, d)
}
}
// 索引命中率
readRequests := cast.ToFloat64(data[9]["value"])
reads := cast.ToFloat64(data[10]["value"])
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
// Innodb 索引命中率
bufferPoolReads := cast.ToFloat64(data[11]["value"])
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
return controllers.Success(ctx, data)
}
// ErrorLog 获取错误日志
func (r *MySQLController) ErrorLog(ctx http.Context) http.Response {
log, err := shell.Execf("tail -n 100 /www/server/mysql/mysql-error.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearErrorLog 清空错误日志
func (r *MySQLController) ClearErrorLog(ctx http.Context) http.Response {
if out, err := shell.Execf("echo '' > /www/server/mysql/mysql-error.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// SlowLog 获取慢查询日志
func (r *MySQLController) SlowLog(ctx http.Context) http.Response {
log, err := shell.Execf("tail -n 100 /www/server/mysql/mysql-slow.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearSlowLog 清空慢查询日志
func (r *MySQLController) ClearSlowLog(ctx http.Context) http.Response {
if out, err := shell.Execf("echo '' > /www/server/mysql/mysql-slow.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// GetRootPassword 获取root密码
func (r *MySQLController) GetRootPassword(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码为空")
}
return controllers.Success(ctx, rootPassword)
}
// SetRootPassword 设置root密码
func (r *MySQLController) SetRootPassword(ctx http.Context) http.Response {
rootPassword := ctx.Request().Input("password")
if len(rootPassword) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "MySQL root密码不能为空")
}
oldRootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
mysql, err := db.NewMySQL("root", oldRootPassword, r.getSock(), "unix")
if err != nil {
// 尝试安全模式直接改密
if err = db.MySQLResetRootPassword(rootPassword); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
} else {
if err = mysql.UserPassword("root", rootPassword); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
}
if err = r.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("设置保存失败: %v", err))
}
return controllers.Success(ctx, nil)
}
// DatabaseList 获取数据库列表
func (r *MySQLController) DatabaseList(ctx http.Context) http.Response {
password := r.setting.Get(models.SettingKeyMysqlRootPassword)
mysql, err := db.NewMySQL("root", password, r.getSock(), "unix")
if err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []types.MySQLDatabase{},
})
}
databases, err := mysql.Databases()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
}
paged, total := controllers.Paginate(ctx, databases)
return controllers.Success(ctx, http.Json{
"total": total,
"items": paged,
})
}
// AddDatabase 添加数据库
func (r *MySQLController) AddDatabase(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:32",
}); sanitize != nil {
return sanitize
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.DatabaseCreate(database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.UserCreate(user, password); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.PrivilegesGrant(user, database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// DeleteDatabase 删除数据库
func (r *MySQLController) DeleteDatabase(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
}); sanitize != nil {
return sanitize
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
database := ctx.Request().Input("database")
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.DatabaseDrop(database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// BackupList 获取备份列表
func (r *MySQLController) BackupList(ctx http.Context) http.Response {
backups, err := r.backup.MysqlList()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
paged, total := controllers.Paginate(ctx, backups)
return controllers.Success(ctx, http.Json{
"total": total,
"items": paged,
})
}
// UploadBackup 上传备份
func (r *MySQLController) UploadBackup(ctx http.Context) http.Response {
file, err := ctx.Request().File("file")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
if !io.Exists(backupPath) {
if err = io.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
return controllers.Success(ctx, nil)
}
// CreateBackup 创建备份
func (r *MySQLController) CreateBackup(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
}); sanitize != nil {
return sanitize
}
database := ctx.Request().Input("database")
if err := r.backup.MysqlBackup(database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// DeleteBackup 删除备份
func (r *MySQLController) DeleteBackup(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"name": "required|min_len:1|max_len:255",
}); sanitize != nil {
return sanitize
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/mysql"
fileName := ctx.Request().Input("name")
if err := io.Remove(backupPath + "/" + fileName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// RestoreBackup 还原备份
func (r *MySQLController) RestoreBackup(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"backup": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
}); sanitize != nil {
return sanitize
}
if err := r.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup")); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// UserList 用户列表
func (r *MySQLController) UserList(ctx http.Context) http.Response {
password := r.setting.Get(models.SettingKeyMysqlRootPassword)
mysql, err := db.NewMySQL("root", password, r.getSock(), "unix")
if err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []types.MySQLUser{},
})
}
users, err := mysql.Users()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
}
paged, total := controllers.Paginate(ctx, users)
return controllers.Success(ctx, http.Json{
"total": total,
"items": paged,
})
}
// AddUser 添加用户
func (r *MySQLController) AddUser(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:32",
}); sanitize != nil {
return sanitize
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.UserCreate(user, password); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.PrivilegesGrant(user, database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// DeleteUser 删除用户
func (r *MySQLController) DeleteUser(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
}); sanitize != nil {
return sanitize
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.UserDrop(user); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// SetUserPassword 设置用户密码
func (r *MySQLController) SetUserPassword(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:32",
}); sanitize != nil {
return sanitize
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.UserPassword(user, password); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// SetUserPrivileges 设置用户权限
func (r *MySQLController) SetUserPrivileges(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"user": "required|min_len:1|max_len:32|regex:^[a-zA-Z0-9_]+$",
"database": "required|min_len:1|max_len:64|regex:^[a-zA-Z0-9_]+$",
}); sanitize != nil {
return sanitize
}
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
user := ctx.Request().Input("user")
database := ctx.Request().Input("database")
mysql, err := db.NewMySQL("root", rootPassword, r.getSock(), "unix")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = mysql.PrivilegesGrant(user, database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// getSock 获取sock文件位置
func (r *MySQLController) getSock() string {
if io.Exists("/tmp/mysql.sock") {
return "/tmp/mysql.sock"
}
if io.Exists("/www/server/mysql/config/my.cnf") {
config, _ := io.Read("/www/server/mysql/config/my.cnf")
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
matches := re.FindStringSubmatch(config)
if len(matches) > 2 {
return matches[2]
}
}
if io.Exists("/etc/my.cnf") {
config, _ := io.Read("/etc/my.cnf")
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
matches := re.FindStringSubmatch(config)
if len(matches) > 2 {
return matches[2]
}
}
return "/tmp/mysql.sock"
}
@@ -1,15 +1,20 @@
package plugins
import (
"fmt"
"regexp"
"time"
"github.com/go-resty/resty/v2"
"github.com/goravel/framework/contracts/http"
"github.com/imroc/req/v3"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/pkg/tools"
"github.com/TheTNB/panel/app/http/controllers"
"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/types"
)
type OpenRestyController struct {
@@ -17,88 +22,67 @@ type OpenRestyController struct {
}
func NewOpenrestyController() *OpenRestyController {
return &OpenRestyController{
// Inject services
}
return &OpenRestyController{}
}
// Status 获取运行状态
func (r *OpenRestyController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("openresty")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
}
return controllers.Success(ctx, status)
}
// Reload 重载配置
func (r *OpenRestyController) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载OpenResty失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动OpenResty
func (r *OpenRestyController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动OpenResty失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止OpenResty
func (r *OpenRestyController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止OpenResty失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启OpenResty
func (r *OpenRestyController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启OpenResty失败")
}
return controllers.Success(ctx, nil)
}
// GetConfig 获取配置
// GetConfig
//
// @Summary 获取配置
// @Tags 插件-OpenResty
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/openresty/config [get]
func (r *OpenRestyController) GetConfig(ctx http.Context) http.Response {
config, err := tools.Read("/www/server/openresty/conf/nginx.conf")
config, err := io.Read("/www/server/openresty/conf/nginx.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty配置失败")
return controllers.Error(ctx, http.StatusInternalServerError, "获取配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
// SaveConfig
//
// @Summary 保存配置
// @Tags 插件-OpenResty
// @Produce json
// @Security BearerToken
// @Param config body string true "配置"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/openresty/config [post]
func (r *OpenRestyController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusInternalServerError, "配置不能为空")
}
if err := tools.Write("/www/server/openresty/conf/nginx.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "保存OpenResty配置失败")
if err := io.Write("/www/server/openresty/conf/nginx.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "保存配置失败")
}
return r.Reload(ctx)
if err := systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载服务失败: %v", err))
}
return controllers.Success(ctx, nil)
}
// ErrorLog 获取错误日志
// ErrorLog
//
// @Summary 获取错误日志
// @Tags 插件-OpenResty
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/openresty/errorLog [get]
func (r *OpenRestyController) ErrorLog(ctx http.Context) http.Response {
if !tools.Exists("/www/wwwlogs/nginx_error.log") {
if !io.Exists("/www/wwwlogs/nginx_error.log") {
return controllers.Success(ctx, "")
}
out, err := tools.Exec("tail -n 100 /www/wwwlogs/nginx_error.log")
out, err := shell.Execf("tail -n 100 /www/wwwlogs/openresty_error.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
@@ -106,52 +90,62 @@ func (r *OpenRestyController) ErrorLog(ctx http.Context) http.Response {
return controllers.Success(ctx, out)
}
// ClearErrorLog 清空错误日志
// ClearErrorLog
//
// @Summary 清空错误日志
// @Tags 插件-OpenResty
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/openresty/clearErrorLog [post]
func (r *OpenRestyController) ClearErrorLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/wwwlogs/nginx_error.log"); err != nil {
if out, err := shell.Execf("echo '' > /www/wwwlogs/openresty_error.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// Load 获取负载
// Load
//
// @Summary 获取负载状态
// @Tags 插件-OpenResty
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/openresty/load [get]
func (r *OpenRestyController) Load(ctx http.Context) http.Response {
client := req.C().SetTimeout(10 * time.Second)
client := resty.New().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/nginx_status")
if err != nil || !resp.IsSuccessState() {
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty负载失败")
if err != nil || !resp.IsSuccess() {
return controllers.Success(ctx, []types.NV{})
}
raw := resp.String()
type nginxStatus struct {
Name string `json:"name"`
Value string `json:"value"`
}
var data []nginxStatus
var data []types.NV
workers, err := tools.Exec("ps aux | grep nginx | grep 'worker process' | wc -l")
workers, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | wc -l")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty负载失败")
return controllers.Error(ctx, http.StatusInternalServerError, "获取负载失败")
}
data = append(data, nginxStatus{
data = append(data, types.NV{
Name: "工作进程",
Value: workers,
})
out, err := tools.Exec("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
out, err := shell.Execf("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty负载失败")
return controllers.Error(ctx, http.StatusInternalServerError, "获取负载失败")
}
mem := tools.FormatBytes(cast.ToFloat64(out))
data = append(data, nginxStatus{
mem := str.FormatBytes(cast.ToFloat64(out))
data = append(data, types.NV{
Name: "内存占用",
Value: mem,
})
match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 2 {
data = append(data, nginxStatus{
data = append(data, types.NV{
Name: "活跃连接数",
Value: match[1],
})
@@ -159,15 +153,15 @@ func (r *OpenRestyController) Load(ctx http.Context) http.Response {
match = regexp.MustCompile(`server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 4 {
data = append(data, nginxStatus{
data = append(data, types.NV{
Name: "总连接次数",
Value: match[1],
})
data = append(data, nginxStatus{
data = append(data, types.NV{
Name: "总握手次数",
Value: match[2],
})
data = append(data, nginxStatus{
data = append(data, types.NV{
Name: "总请求次数",
Value: match[3],
})
@@ -175,15 +169,15 @@ func (r *OpenRestyController) Load(ctx http.Context) http.Response {
match = regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`).FindStringSubmatch(raw)
if len(match) == 4 {
data = append(data, nginxStatus{
data = append(data, types.NV{
Name: "请求数",
Value: match[1],
})
data = append(data, nginxStatus{
data = append(data, types.NV{
Name: "响应数",
Value: match[2],
})
data = append(data, nginxStatus{
data = append(data, types.NV{
Name: "驻留进程",
Value: match[3],
})
@@ -1,280 +0,0 @@
package plugins
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/imroc/req/v3"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Php74Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp74Controller() *Php74Controller {
return &Php74Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "74",
}
}
func (r *Php74Controller) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("php-fpm-" + r.version)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"运行状态失败")
}
return controllers.Success(ctx, status)
}
func (r *Php74Controller) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php74Controller) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php74Controller) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php74Controller) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php74Controller) GetConfig(ctx http.Context) http.Response {
config, err := tools.Read("/www/server/php/" + r.version + "/etc/php.ini")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"配置失败")
}
return controllers.Success(ctx, config)
}
func (r *Php74Controller) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if err := tools.Write("/www/server/php/"+r.version+"/etc/php.ini", config, 0644); err != nil {
return nil
}
return r.Reload(ctx)
}
func (r *Php74Controller) Load(ctx http.Context) http.Response {
client := req.C().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + r.version)
if err != nil || !resp.IsSuccessState() {
facades.Log().Info("获取PHP-" + r.version + "运行状态失败")
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+r.version+"] 获取运行状态失败")
}
raw := resp.String()
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
type Data struct {
Name string `json:"name"`
Value string `json:"value"`
}
data := make([]Data, len(dataKeys))
for i := range dataKeys {
data[i].Name = dataKeys[i]
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
match := r.FindStringSubmatch(raw)
if len(match) > 1 {
data[i].Value = strings.TrimSpace(match[1])
}
}
return controllers.Success(ctx, data)
}
func (r *Php74Controller) ErrorLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/php-fpm.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
func (r *Php74Controller) SlowLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/slow.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
func (r *Php74Controller) ClearErrorLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/php-fpm.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
func (r *Php74Controller) ClearSlowLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/slow.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
func (r *Php74Controller) GetExtensionList(ctx http.Context) http.Response {
extensions := r.GetExtensions()
return controllers.Success(ctx, extensions)
}
func (r *Php74Controller) InstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
extensions := r.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if item.Installed {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展已安装")
}
var task models.Task
task.Name = "安装PHP-" + r.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Info("[PHP-" + r.version + "] 创建安装拓展任务失败:" + err.Error())
return controllers.ErrorSystem(ctx)
}
r.task.Process(task.ID)
return controllers.Success(ctx, true)
}
}
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
}
func (r *Php74Controller) UninstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
extensions := r.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if !item.Installed {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展未安装")
}
var task models.Task
task.Name = "卸载PHP-" + r.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Info("[PHP-" + r.version + "] 创建卸载拓展任务失败:" + err.Error())
return controllers.ErrorSystem(ctx)
}
r.task.Process(task.ID)
return controllers.Success(ctx, true)
}
}
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
}
func (r *Php74Controller) GetExtensions() []PHPExtension {
var extensions []PHPExtension
extensions = append(extensions, PHPExtension{
Name: "OPcache",
Slug: "Zend OPcache",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "PhpRedis",
Slug: "redis",
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "ImageMagick",
Slug: "imagick",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "Exif",
Slug: "exif",
Description: "通过 exif 扩展,你可以操作图像元数据。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: "(需先安装PostgreSQLpdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "ionCube",
Slug: "ionCube Loader",
Description: "ionCube 是一个专业级的PHP加密解密工具。",
Installed: false,
})
raw, err := tools.Exec("/www/server/php/" + r.version + "/bin/php -m")
if err != nil {
return extensions
}
rawExtensionList := strings.Split(raw, "\n")
for _, item := range rawExtensionList {
if !strings.Contains(item, "[") && item != "" {
for i := range extensions {
if extensions[i].Slug == item {
extensions[i].Installed = true
}
}
}
}
return extensions
}
@@ -1,280 +0,0 @@
package plugins
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/imroc/req/v3"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Php80Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp80Controller() *Php80Controller {
return &Php80Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "80",
}
}
func (r *Php80Controller) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("php-fpm-" + r.version)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"运行状态失败")
}
return controllers.Success(ctx, status)
}
func (r *Php80Controller) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php80Controller) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php80Controller) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php80Controller) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php80Controller) GetConfig(ctx http.Context) http.Response {
config, err := tools.Read("/www/server/php/" + r.version + "/etc/php.ini")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"配置失败")
}
return controllers.Success(ctx, config)
}
func (r *Php80Controller) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if err := tools.Write("/www/server/php/"+r.version+"/etc/php.ini", config, 0644); err != nil {
return nil
}
return r.Reload(ctx)
}
func (r *Php80Controller) Load(ctx http.Context) http.Response {
client := req.C().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + r.version)
if err != nil || !resp.IsSuccessState() {
facades.Log().Info("获取PHP-" + r.version + "运行状态失败")
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+r.version+"] 获取运行状态失败")
}
raw := resp.String()
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
type Data struct {
Name string `json:"name"`
Value string `json:"value"`
}
data := make([]Data, len(dataKeys))
for i := range dataKeys {
data[i].Name = dataKeys[i]
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
match := r.FindStringSubmatch(raw)
if len(match) > 1 {
data[i].Value = strings.TrimSpace(match[1])
}
}
return controllers.Success(ctx, data)
}
func (r *Php80Controller) ErrorLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/php-fpm.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
func (r *Php80Controller) SlowLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/slow.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
func (r *Php80Controller) ClearErrorLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/php-fpm.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
func (r *Php80Controller) ClearSlowLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/slow.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
func (r *Php80Controller) GetExtensionList(ctx http.Context) http.Response {
extensions := r.GetExtensions()
return controllers.Success(ctx, extensions)
}
func (r *Php80Controller) InstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
extensions := r.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if item.Installed {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展已安装")
}
var task models.Task
task.Name = "安装PHP-" + r.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Info("[PHP-" + r.version + "] 创建安装拓展任务失败:" + err.Error())
return controllers.ErrorSystem(ctx)
}
r.task.Process(task.ID)
return controllers.Success(ctx, true)
}
}
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
}
func (r *Php80Controller) UninstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
extensions := r.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if !item.Installed {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展未安装")
}
var task models.Task
task.Name = "卸载PHP-" + r.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Info("[PHP-" + r.version + "] 创建卸载拓展任务失败:" + err.Error())
return controllers.ErrorSystem(ctx)
}
r.task.Process(task.ID)
return controllers.Success(ctx, true)
}
}
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
}
func (r *Php80Controller) GetExtensions() []PHPExtension {
var extensions []PHPExtension
extensions = append(extensions, PHPExtension{
Name: "OPcache",
Slug: "Zend OPcache",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "PhpRedis",
Slug: "redis",
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "ImageMagick",
Slug: "imagick",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "Exif",
Slug: "exif",
Description: "通过 exif 扩展,你可以操作图像元数据。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: "(需先安装PostgreSQLpdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "ionCube",
Slug: "ionCube Loader",
Description: "ionCube 是一个专业级的PHP加密解密工具。",
Installed: false,
})
raw, err := tools.Exec("/www/server/php/" + r.version + "/bin/php -m")
if err != nil {
return extensions
}
rawExtensionList := strings.Split(raw, "\n")
for _, item := range rawExtensionList {
if !strings.Contains(item, "[") && item != "" {
for i := range extensions {
if extensions[i].Slug == item {
extensions[i].Installed = true
}
}
}
}
return extensions
}
@@ -1,280 +0,0 @@
package plugins
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/imroc/req/v3"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Php81Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp81Controller() *Php81Controller {
return &Php81Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "81",
}
}
func (r *Php81Controller) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("php-fpm-" + r.version)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"运行状态失败")
}
return controllers.Success(ctx, status)
}
func (r *Php81Controller) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php81Controller) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php81Controller) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php81Controller) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php81Controller) GetConfig(ctx http.Context) http.Response {
config, err := tools.Read("/www/server/php/" + r.version + "/etc/php.ini")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"配置失败")
}
return controllers.Success(ctx, config)
}
func (r *Php81Controller) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if err := tools.Write("/www/server/php/"+r.version+"/etc/php.ini", config, 0644); err != nil {
return nil
}
return r.Reload(ctx)
}
func (r *Php81Controller) Load(ctx http.Context) http.Response {
client := req.C().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + r.version)
if err != nil || !resp.IsSuccessState() {
facades.Log().Info("获取PHP-" + r.version + "运行状态失败")
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+r.version+"] 获取运行状态失败")
}
raw := resp.String()
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
type Data struct {
Name string `json:"name"`
Value string `json:"value"`
}
data := make([]Data, len(dataKeys))
for i := range dataKeys {
data[i].Name = dataKeys[i]
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
match := r.FindStringSubmatch(raw)
if len(match) > 1 {
data[i].Value = strings.TrimSpace(match[1])
}
}
return controllers.Success(ctx, data)
}
func (r *Php81Controller) ErrorLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/php-fpm.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
func (r *Php81Controller) SlowLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/slow.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
func (r *Php81Controller) ClearErrorLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/php-fpm.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
func (r *Php81Controller) ClearSlowLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/slow.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
func (r *Php81Controller) GetExtensionList(ctx http.Context) http.Response {
extensions := r.GetExtensions()
return controllers.Success(ctx, extensions)
}
func (r *Php81Controller) InstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
extensions := r.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if item.Installed {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展已安装")
}
var task models.Task
task.Name = "安装PHP-" + r.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Info("[PHP-" + r.version + "] 创建安装拓展任务失败:" + err.Error())
return controllers.ErrorSystem(ctx)
}
r.task.Process(task.ID)
return controllers.Success(ctx, true)
}
}
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
}
func (r *Php81Controller) UninstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
extensions := r.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if !item.Installed {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展未安装")
}
var task models.Task
task.Name = "卸载PHP-" + r.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Info("[PHP-" + r.version + "] 创建卸载拓展任务失败:" + err.Error())
return controllers.ErrorSystem(ctx)
}
r.task.Process(task.ID)
return controllers.Success(ctx, true)
}
}
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
}
func (r *Php81Controller) GetExtensions() []PHPExtension {
var extensions []PHPExtension
extensions = append(extensions, PHPExtension{
Name: "OPcache",
Slug: "Zend OPcache",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "PhpRedis",
Slug: "redis",
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "ImageMagick",
Slug: "imagick",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "Exif",
Slug: "exif",
Description: "通过 exif 扩展,你可以操作图像元数据。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: "(需先安装PostgreSQLpdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "ionCube",
Slug: "ionCube Loader",
Description: "ionCube 是一个专业级的PHP加密解密工具。",
Installed: false,
})
raw, err := tools.Exec("/www/server/php/" + r.version + "/bin/php -m")
if err != nil {
return extensions
}
rawExtensionList := strings.Split(raw, "\n")
for _, item := range rawExtensionList {
if !strings.Contains(item, "[") && item != "" {
for i := range extensions {
if extensions[i].Slug == item {
extensions[i].Installed = true
}
}
}
}
return extensions
}
@@ -1,274 +0,0 @@
package plugins
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/imroc/req/v3"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Php82Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp82Controller() *Php82Controller {
return &Php82Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "82",
}
}
func (r *Php82Controller) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("php-fpm-" + r.version)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"运行状态失败")
}
return controllers.Success(ctx, status)
}
func (r *Php82Controller) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php82Controller) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php82Controller) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php82Controller) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("php-fpm-" + r.version); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启PHP-"+r.version+"失败")
}
return controllers.Success(ctx, nil)
}
func (r *Php82Controller) GetConfig(ctx http.Context) http.Response {
config, err := tools.Read("/www/server/php/" + r.version + "/etc/php.ini")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+r.version+"配置失败")
}
return controllers.Success(ctx, config)
}
func (r *Php82Controller) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if err := tools.Write("/www/server/php/"+r.version+"/etc/php.ini", config, 0644); err != nil {
return nil
}
return r.Reload(ctx)
}
func (r *Php82Controller) Load(ctx http.Context) http.Response {
client := req.C().SetTimeout(10 * time.Second)
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + r.version)
if err != nil || !resp.IsSuccessState() {
facades.Log().Info("获取PHP-" + r.version + "运行状态失败")
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+r.version+"] 获取运行状态失败")
}
raw := resp.String()
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
type Data struct {
Name string `json:"name"`
Value string `json:"value"`
}
data := make([]Data, len(dataKeys))
for i := range dataKeys {
data[i].Name = dataKeys[i]
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
match := r.FindStringSubmatch(raw)
if len(match) > 1 {
data[i].Value = strings.TrimSpace(match[1])
}
}
return controllers.Success(ctx, data)
}
func (r *Php82Controller) ErrorLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/php-fpm.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
func (r *Php82Controller) SlowLog(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/php/" + r.version + "/var/log/slow.log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
func (r *Php82Controller) ClearErrorLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/php-fpm.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
func (r *Php82Controller) ClearSlowLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/php/" + r.version + "/var/log/slow.log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
func (r *Php82Controller) GetExtensionList(ctx http.Context) http.Response {
extensions := r.GetExtensions()
return controllers.Success(ctx, extensions)
}
func (r *Php82Controller) InstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
extensions := r.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if item.Installed {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展已安装")
}
var task models.Task
task.Name = "安装PHP-" + r.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Info("[PHP-" + r.version + "] 创建安装拓展任务失败:" + err.Error())
return controllers.ErrorSystem(ctx)
}
r.task.Process(task.ID)
return controllers.Success(ctx, true)
}
}
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
}
func (r *Php82Controller) UninstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
extensions := r.GetExtensions()
for _, item := range extensions {
if item.Slug == slug {
if !item.Installed {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展未安装")
}
var task models.Task
task.Name = "卸载PHP-" + r.version + "扩展-" + item.Name
task.Status = models.TaskStatusWaiting
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + r.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
task.Log = "/tmp/" + item.Slug + ".log"
if err := facades.Orm().Query().Create(&task); err != nil {
facades.Log().Info("[PHP-" + r.version + "] 创建卸载拓展任务失败:" + err.Error())
return controllers.ErrorSystem(ctx)
}
r.task.Process(task.ID)
return controllers.Success(ctx, true)
}
}
return controllers.Error(ctx, http.StatusUnprocessableEntity, "扩展不存在")
}
func (r *Php82Controller) GetExtensions() []PHPExtension {
var extensions []PHPExtension
extensions = append(extensions, PHPExtension{
Name: "OPcache",
Slug: "Zend OPcache",
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "PhpRedis",
Slug: "redis",
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "ImageMagick",
Slug: "imagick",
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "Exif",
Slug: "exif",
Description: "通过 exif 扩展,你可以操作图像元数据。",
Installed: false,
})
extensions = append(extensions, PHPExtension{
Name: "pdo_pgsql",
Slug: "pdo_pgsql",
Description: "(需先安装PostgreSQLpdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
Installed: false,
})
raw, err := tools.Exec("/www/server/php/" + r.version + "/bin/php -m")
if err != nil {
return extensions
}
rawExtensionList := strings.Split(raw, "\n")
for _, item := range rawExtensionList {
if !strings.Contains(item, "[") && item != "" {
for i := range extensions {
if extensions[i].Slug == item {
extensions[i].Installed = true
}
}
}
}
return extensions
}
@@ -0,0 +1,246 @@
package plugins
import (
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/internal/services"
)
type PHPController struct{}
func NewPHPController() *PHPController {
return &PHPController{}
}
// GetConfig
//
// @Summary 获取配置
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/config [get]
func (r *PHPController) GetConfig(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
config, err := service.GetConfig()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
// SaveConfig
//
// @Summary 保存配置
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Param config body string true "配置"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/config [post]
func (r *PHPController) SaveConfig(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
config := ctx.Request().Input("config")
if err := service.SaveConfig(config); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// GetFPMConfig
//
// @Summary 获取 FPM 配置
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/fpmConfig [get]
func (r *PHPController) GetFPMConfig(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
config, err := service.GetFPMConfig()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
// SaveFPMConfig
//
// @Summary 保存 FPM 配置
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Param config body string true "配置"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/fpmConfig [post]
func (r *PHPController) SaveFPMConfig(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
config := ctx.Request().Input("config")
if err := service.SaveFPMConfig(config); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// Load
//
// @Summary 获取负载状态
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/load [get]
func (r *PHPController) Load(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
load, err := service.Load()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, load)
}
// ErrorLog
//
// @Summary 获取错误日志
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/errorLog [get]
func (r *PHPController) ErrorLog(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
log, _ := service.GetErrorLog()
return controllers.Success(ctx, log)
}
// SlowLog
//
// @Summary 获取慢日志
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/slowLog [get]
func (r *PHPController) SlowLog(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
log, _ := service.GetSlowLog()
return controllers.Success(ctx, log)
}
// ClearErrorLog
//
// @Summary 清空错误日志
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/clearErrorLog [post]
func (r *PHPController) ClearErrorLog(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
err := service.ClearErrorLog()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// ClearSlowLog
//
// @Summary 清空慢日志
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/clearSlowLog [post]
func (r *PHPController) ClearSlowLog(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
err := service.ClearSlowLog()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// ExtensionList
//
// @Summary 获取扩展列表
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/extensions [get]
func (r *PHPController) ExtensionList(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
extensions, err := service.GetExtensions()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, extensions)
}
// InstallExtension
//
// @Summary 安装扩展
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Param slug query string true "slug"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/extensions [post]
func (r *PHPController) InstallExtension(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
if err := service.InstallExtension(slug); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// UninstallExtension
//
// @Summary 卸载扩展
// @Tags 插件-PHP
// @Produce json
// @Security BearerToken
// @Param version path int true "PHP 版本"
// @Param slug query string true "slug"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/php/{version}/extensions [delete]
func (r *PHPController) UninstallExtension(ctx http.Context) http.Response {
service := services.NewPHPImpl(uint(ctx.Request().RouteInt("version")))
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
if err := service.UninstallExtension(slug); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
@@ -1,7 +1,7 @@
package plugins
import (
"os"
"fmt"
"regexp"
"strings"
@@ -9,8 +9,11 @@ import (
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/pkg/tools"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/os"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
)
type PhpMyAdminController struct {
@@ -21,7 +24,7 @@ func NewPhpMyAdminController() *PhpMyAdminController {
}
func (r *PhpMyAdminController) Info(ctx http.Context) http.Response {
files, err := os.ReadDir("/www/server/phpmyadmin")
files, err := io.ReadDir("/www/server/phpmyadmin")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 目录")
}
@@ -36,7 +39,7 @@ func (r *PhpMyAdminController) Info(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusInternalServerError, "找不到 phpMyAdmin 目录")
}
conf, err := tools.Read("/www/server/vhost/phpmyadmin.conf")
conf, err := io.Read("/www/server/vhost/phpmyadmin.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
@@ -52,41 +55,72 @@ func (r *PhpMyAdminController) Info(ctx http.Context) http.Response {
}
func (r *PhpMyAdminController) SetPort(ctx http.Context) http.Response {
port := ctx.Request().Input("port")
if len(port) == 0 {
port := ctx.Request().InputInt("port")
if port == 0 {
return controllers.Error(ctx, http.StatusInternalServerError, "端口不能为空")
}
conf, err := tools.Read("/www/server/vhost/phpmyadmin.conf")
conf, err := io.Read("/www/server/vhost/phpmyadmin.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+port+";")
if err := tools.Write("/www/server/vhost/phpmyadmin.conf", conf, 0644); err != nil {
conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+cast.ToString(port)+";")
if err := io.Write("/www/server/vhost/phpmyadmin.conf", conf, 0644); err != nil {
facades.Log().Request(ctx.Request()).Tags("插件", "phpMyAdmin").With(map[string]any{
"error": err.Error(),
}).Info("修改 phpMyAdmin 端口失败")
return controllers.ErrorSystem(ctx)
}
if tools.IsRHEL() {
if out, err := tools.Exec("firewall-cmd --zone=public --add-port=" + port + "/tcp --permanent"); err != nil {
if os.IsRHEL() {
if out, err := shell.Execf("firewall-cmd --zone=public --add-port=%d/tcp --permanent", port); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --reload"); err != nil {
if out, err := shell.Execf("firewall-cmd --reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := tools.Exec("ufw allow " + port + "/tcp"); err != nil {
if out, err := shell.Execf("ufw allow %d/tcp", port); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw reload"); err != nil {
if out, err := shell.Execf("ufw reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
}
if err := tools.ServiceReload("openresty"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载OpenResty失败")
if err = systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return controllers.Success(ctx, nil)
}
func (r *PhpMyAdminController) GetConfig(ctx http.Context) http.Response {
config, err := io.Read("/www/server/vhost/phpmyadmin.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
func (r *PhpMyAdminController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusInternalServerError, "配置不能为空")
}
if err := io.Write("/www/server/vhost/phpmyadmin.conf", config, 0644); err != nil {
facades.Log().Request(ctx.Request()).Tags("插件", "phpMyAdmin").With(map[string]any{
"error": err.Error(),
}).Info("修改 phpMyAdmin 配置失败")
return controllers.ErrorSystem(ctx)
}
if err := systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return controllers.Success(ctx, nil)
@@ -0,0 +1,109 @@
package plugins
import (
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/app/http/controllers"
requests "github.com/TheTNB/panel/app/http/requests/plugins/podman"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/systemctl"
)
type PodmanController struct {
}
func NewPodmanController() *PodmanController {
return &PodmanController{}
}
// GetRegistryConfig
//
// @Summary 获取注册表配置
// @Description 获取 Podman 注册表配置
// @Tags 插件-Podman
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/podman/registryConfig [get]
func (r *PodmanController) GetRegistryConfig(ctx http.Context) http.Response {
config, err := io.Read("/etc/containers/registries.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
// UpdateRegistryConfig
//
// @Summary 更新注册表配置
// @Description 更新 Podman 注册表配置
// @Tags 插件-Podman
// @Produce json
// @Security BearerToken
// @Param data body requests.UpdateRegistryConfig true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/podman/registryConfig [post]
func (r *PodmanController) UpdateRegistryConfig(ctx http.Context) http.Response {
var updateRequest requests.UpdateRegistryConfig
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
if err := io.Write("/etc/containers/registries.conf", updateRequest.Config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := systemctl.Restart("podman"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// GetStorageConfig
//
// @Summary 获取存储配置
// @Description 获取 Podman 存储配置
// @Tags 插件-Podman
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/podman/storageConfig [get]
func (r *PodmanController) GetStorageConfig(ctx http.Context) http.Response {
config, err := io.Read("/etc/containers/storage.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
// UpdateStorageConfig
//
// @Summary 更新存储配置
// @Description 更新 Podman 存储配置
// @Tags 插件-Podman
// @Produce json
// @Security BearerToken
// @Param data body requests.UpdateStorageConfig true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/podman/storageConfig [post]
func (r *PodmanController) UpdateStorageConfig(ctx http.Context) http.Response {
var updateRequest requests.UpdateStorageConfig
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
if err := io.Write("/etc/containers/storage.conf", updateRequest.Config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := systemctl.Restart("podman"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
@@ -1,551 +0,0 @@
package plugins
import (
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/support/carbon"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Postgresql15Controller struct {
setting services.Setting
backup services.Backup
}
func NewPostgresql15Controller() *Postgresql15Controller {
return &Postgresql15Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// Status 获取运行状态
func (r *Postgresql15Controller) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
return controllers.Success(ctx, status)
}
// Reload 重载配置
func (r *Postgresql15Controller) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启服务
func (r *Postgresql15Controller) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *Postgresql15Controller) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *Postgresql15Controller) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// GetConfig 获取配置
func (r *Postgresql15Controller) GetConfig(ctx http.Context) http.Response {
// 获取配置
config, err := tools.Read("/www/server/postgresql/data/postgresql.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
}
return controllers.Success(ctx, config)
}
// GetUserConfig 获取用户配置
func (r *Postgresql15Controller) GetUserConfig(ctx http.Context) http.Response {
// 获取配置
config, err := tools.Read("/www/server/postgresql/data/pg_hba.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *Postgresql15Controller) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/postgresql/data/postgresql.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
}
return r.Restart(ctx)
}
// SaveUserConfig 保存用户配置
func (r *Postgresql15Controller) SaveUserConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
}
return r.Restart(ctx)
}
// Load 获取负载
func (r *Postgresql15Controller) Load(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL已停止运行")
}
time, err := tools.Exec(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL启动时间失败")
}
pid, err := tools.Exec(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程PID失败")
}
process, err := tools.Exec(`ps aux | grep postgres | grep -v grep | wc -l`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程数失败")
}
connections, err := tools.Exec(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL连接数失败")
}
storage, err := tools.Exec(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL空间占用失败")
}
data := []LoadInfo{
{"启动时间", carbon.Parse(time).ToDateTimeString()},
{"进程 PID", pid},
{"进程数", process},
{"总连接数", connections},
{"空间占用", storage},
}
return controllers.Success(ctx, data)
}
// Log 获取日志
func (r *Postgresql15Controller) Log(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearLog 清空日志
func (r *Postgresql15Controller) ClearLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// DatabaseList 获取数据库列表
func (r *Postgresql15Controller) DatabaseList(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL已停止运行")
}
type database struct {
Name string `json:"name"`
Owner string `json:"owner"`
Encoding string `json:"encoding"`
}
raw, err := tools.Exec(`echo "\l" | su - postgres -c "psql"`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, raw)
}
databases := strings.Split(raw, "\n")
if len(databases) >= 4 {
databases = databases[3 : len(databases)-1]
} else {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
var databaseList []database
for _, db := range databases {
parts := strings.Split(db, "|")
if len(parts) != 8 || len(strings.TrimSpace(parts[0])) == 0 {
continue
}
databaseList = append(databaseList, database{
Name: strings.TrimSpace(parts[0]),
Owner: strings.TrimSpace(parts[1]),
Encoding: strings.TrimSpace(parts[2]),
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(databaseList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
if endIndex > len(databaseList) {
endIndex = len(databaseList)
}
pagedDatabases := databaseList[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(databaseList),
"items": pagedDatabases,
})
}
// AddDatabase 添加数据库
func (r *Postgresql15Controller) AddDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec(`echo "CREATE DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
if out, err := tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// DeleteDatabase 删除数据库
func (r *Postgresql15Controller) DeleteDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
if out, err := tools.Exec(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// BackupList 获取备份列表
func (r *Postgresql15Controller) BackupList(ctx http.Context) http.Response {
backupList, err := r.backup.PostgresqlList()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(backupList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []services.BackupFile{},
})
}
if endIndex > len(backupList) {
endIndex = len(backupList)
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []services.BackupFile{}
}
return controllers.Success(ctx, http.Json{
"total": len(backupList),
"items": pagedBackupList,
})
}
// UploadBackup 上传备份
func (r *Postgresql15Controller) UploadBackup(ctx http.Context) http.Response {
file, err := ctx.Request().File("file")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
if !tools.Exists(backupPath) {
if err = tools.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
return controllers.Success(ctx, nil)
}
// CreateBackup 创建备份
func (r *Postgresql15Controller) CreateBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
err = r.backup.PostgresqlBackup(database)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// DeleteBackup 删除备份
func (r *Postgresql15Controller) DeleteBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
fileName := ctx.Request().Input("name")
if err := tools.Remove(backupPath + "/" + fileName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// RestoreBackup 还原备份
func (r *Postgresql15Controller) RestoreBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"backup": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
err = r.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup"))
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
}
return controllers.Success(ctx, nil)
}
// UserList 用户列表
func (r *Postgresql15Controller) UserList(ctx http.Context) http.Response {
type user struct {
User string `json:"user"`
Role string `json:"role"`
}
raw, err := tools.Exec(`echo "\du" | su - postgres -c "psql"`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, raw)
}
users := strings.Split(raw, "\n")
if len(users) < 4 {
return controllers.Error(ctx, http.StatusInternalServerError, "用户列表为空")
}
users = users[3:]
var userList []user
for _, u := range users {
userInfo := strings.Split(u, "|")
if len(userInfo) != 3 {
continue
}
userList = append(userList, user{
User: strings.TrimSpace(userInfo[0]),
Role: strings.TrimSpace(userInfo[1]),
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(userList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
})
}
if endIndex > len(userList) {
endIndex = len(userList)
}
pagedUsers := userList[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(userList),
"items": pagedUsers,
})
}
// AddUser 添加用户
func (r *Postgresql15Controller) AddUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
if out, err := tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
if out, err := tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// DeleteUser 删除用户
func (r *Postgresql15Controller) DeleteUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
if out, err := tools.Exec(`echo "DROP USER ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// SetUserPassword 设置用户密码
func (r *Postgresql15Controller) SetUserPassword(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
@@ -1,551 +0,0 @@
package plugins
import (
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/support/carbon"
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
)
type Postgresql16Controller struct {
setting services.Setting
backup services.Backup
}
func NewPostgresql16Controller() *Postgresql16Controller {
return &Postgresql16Controller{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// Status 获取运行状态
func (r *Postgresql16Controller) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
return controllers.Success(ctx, status)
}
// Reload 重载配置
func (r *Postgresql16Controller) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启服务
func (r *Postgresql16Controller) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *Postgresql16Controller) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *Postgresql16Controller) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止PostgreSQL失败")
}
return controllers.Success(ctx, nil)
}
// GetConfig 获取配置
func (r *Postgresql16Controller) GetConfig(ctx http.Context) http.Response {
// 获取配置
config, err := tools.Read("/www/server/postgresql/data/postgresql.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
}
return controllers.Success(ctx, config)
}
// GetUserConfig 获取用户配置
func (r *Postgresql16Controller) GetUserConfig(ctx http.Context) http.Response {
// 获取配置
config, err := tools.Read("/www/server/postgresql/data/pg_hba.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *Postgresql16Controller) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/postgresql/data/postgresql.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
}
return r.Restart(ctx)
}
// SaveUserConfig 保存用户配置
func (r *Postgresql16Controller) SaveUserConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
}
return r.Restart(ctx)
}
// Load 获取负载
func (r *Postgresql16Controller) Load(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL已停止运行")
}
time, err := tools.Exec(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL启动时间失败")
}
pid, err := tools.Exec(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程PID失败")
}
process, err := tools.Exec(`ps aux | grep postgres | grep -v grep | wc -l`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程数失败")
}
connections, err := tools.Exec(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL连接数失败")
}
storage, err := tools.Exec(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL空间占用失败")
}
data := []LoadInfo{
{"启动时间", carbon.Parse(time).ToDateTimeString()},
{"进程 PID", pid},
{"进程数", process},
{"总连接数", connections},
{"空间占用", storage},
}
return controllers.Success(ctx, data)
}
// Log 获取日志
func (r *Postgresql16Controller) Log(ctx http.Context) http.Response {
log, err := tools.Exec("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearLog 清空日志
func (r *Postgresql16Controller) ClearLog(ctx http.Context) http.Response {
if out, err := tools.Exec("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// DatabaseList 获取数据库列表
func (r *Postgresql16Controller) DatabaseList(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("postgresql")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
}
if !status {
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL已停止运行")
}
type database struct {
Name string `json:"name"`
Owner string `json:"owner"`
Encoding string `json:"encoding"`
}
raw, err := tools.Exec(`echo "\l" | su - postgres -c "psql"`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, raw)
}
databases := strings.Split(raw, "\n")
if len(databases) >= 4 {
databases = databases[3 : len(databases)-1]
} else {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
var databaseList []database
for _, db := range databases {
parts := strings.Split(db, "|")
if len(parts) != 9 || len(strings.TrimSpace(parts[0])) == 0 {
continue
}
databaseList = append(databaseList, database{
Name: strings.TrimSpace(parts[0]),
Owner: strings.TrimSpace(parts[1]),
Encoding: strings.TrimSpace(parts[2]),
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(databaseList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
if endIndex > len(databaseList) {
endIndex = len(databaseList)
}
pagedDatabases := databaseList[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(databaseList),
"items": pagedDatabases,
})
}
// AddDatabase 添加数据库
func (r *Postgresql16Controller) AddDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec(`echo "CREATE DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
if out, err := tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// DeleteDatabase 删除数据库
func (r *Postgresql16Controller) DeleteDatabase(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
if out, err := tools.Exec(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// BackupList 获取备份列表
func (r *Postgresql16Controller) BackupList(ctx http.Context) http.Response {
backupList, err := r.backup.PostgresqlList()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(backupList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []services.BackupFile{},
})
}
if endIndex > len(backupList) {
endIndex = len(backupList)
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []services.BackupFile{}
}
return controllers.Success(ctx, http.Json{
"total": len(backupList),
"items": pagedBackupList,
})
}
// UploadBackup 上传备份
func (r *Postgresql16Controller) UploadBackup(ctx http.Context) http.Response {
file, err := ctx.Request().File("file")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
if !tools.Exists(backupPath) {
if err = tools.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
return controllers.Success(ctx, nil)
}
// CreateBackup 创建备份
func (r *Postgresql16Controller) CreateBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
database := ctx.Request().Input("database")
err = r.backup.PostgresqlBackup(database)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// DeleteBackup 删除备份
func (r *Postgresql16Controller) DeleteBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
fileName := ctx.Request().Input("name")
if err := tools.Remove(backupPath + "/" + fileName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// RestoreBackup 还原备份
func (r *Postgresql16Controller) RestoreBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"backup": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
err = r.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup"))
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
}
return controllers.Success(ctx, nil)
}
// UserList 用户列表
func (r *Postgresql16Controller) UserList(ctx http.Context) http.Response {
type user struct {
User string `json:"user"`
Role string `json:"role"`
}
raw, err := tools.Exec(`echo "\du" | su - postgres -c "psql"`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, raw)
}
users := strings.Split(raw, "\n")
if len(users) < 4 {
return controllers.Error(ctx, http.StatusInternalServerError, "用户列表为空")
}
users = users[3:]
var userList []user
for _, u := range users {
userInfo := strings.Split(u, "|")
if len(userInfo) != 2 {
continue
}
userList = append(userList, user{
User: strings.TrimSpace(userInfo[0]),
Role: strings.TrimSpace(userInfo[1]),
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(userList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []user{},
})
}
if endIndex > len(userList) {
endIndex = len(userList)
}
pagedUsers := userList[startIndex:endIndex]
return controllers.Success(ctx, http.Json{
"total": len(userList),
"items": pagedUsers,
})
}
// AddUser 添加用户
func (r *Postgresql16Controller) AddUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
if out, err := tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
if out, err := tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// DeleteUser 删除用户
func (r *Postgresql16Controller) DeleteUser(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
if out, err := tools.Exec(`echo "DROP USER ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return r.Reload(ctx)
}
// SetUserPassword 设置用户密码
func (r *Postgresql16Controller) SetUserPassword(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:255",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := tools.Exec(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
@@ -0,0 +1,491 @@
package plugins
import (
"database/sql"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/support/carbon"
_ "github.com/lib/pq"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"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 PostgreSQLController struct {
setting internal.Setting
backup internal.Backup
}
func NewPostgreSQLController() *PostgreSQLController {
return &PostgreSQLController{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// GetConfig 获取配置
func (r *PostgreSQLController) GetConfig(ctx http.Context) http.Response {
// 获取配置
config, err := io.Read("/www/server/postgresql/data/postgresql.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
}
return controllers.Success(ctx, config)
}
// GetUserConfig 获取用户配置
func (r *PostgreSQLController) GetUserConfig(ctx http.Context) http.Response {
// 获取配置
config, err := io.Read("/www/server/postgresql/data/pg_hba.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
}
return controllers.Success(ctx, config)
}
// SaveConfig 保存配置
func (r *PostgreSQLController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := io.Write("/www/server/postgresql/data/postgresql.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
}
if err := systemctl.Reload("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
}
return controllers.Success(ctx, nil)
}
// SaveUserConfig 保存用户配置
func (r *PostgreSQLController) SaveUserConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := io.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
}
if err := systemctl.Reload("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
}
return controllers.Success(ctx, nil)
}
// Load 获取负载
func (r *PostgreSQLController) Load(ctx http.Context) http.Response {
status, _ := systemctl.Status("postgresql")
if !status {
return controllers.Success(ctx, []types.NV{})
}
time, err := shell.Execf(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL启动时间失败")
}
pid, err := shell.Execf(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程PID失败")
}
process, err := shell.Execf(`ps aux | grep postgres | grep -v grep | wc -l`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL进程数失败")
}
connections, err := shell.Execf(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL连接数失败")
}
storage, err := shell.Execf(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL空间占用失败")
}
data := []types.NV{
{Name: "启动时间", Value: carbon.Parse(time).ToDateTimeString()},
{Name: "进程 PID", Value: pid},
{Name: "进程数", Value: process},
{Name: "总连接数", Value: connections},
{Name: "空间占用", Value: storage},
}
return controllers.Success(ctx, data)
}
// Log 获取日志
func (r *PostgreSQLController) Log(ctx http.Context) http.Response {
log, err := shell.Execf("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
return controllers.Success(ctx, log)
}
// ClearLog 清空日志
func (r *PostgreSQLController) ClearLog(ctx http.Context) http.Response {
if out, err := shell.Execf("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// DatabaseList 获取数据库列表
func (r *PostgreSQLController) DatabaseList(ctx http.Context) http.Response {
type database struct {
Name string `json:"name"`
Owner string `json:"owner"`
Encoding string `json:"encoding"`
}
db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres dbname=postgres sslmode=disable")
if err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
if err = db.Ping(); err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []database{},
})
}
query := `
SELECT d.datname, pg_catalog.pg_get_userbyid(d.datdba), pg_catalog.pg_encoding_to_char(d.encoding)
FROM pg_catalog.pg_database d
WHERE datistemplate = false;
`
rows, err := db.Query(query)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var databases []database
for rows.Next() {
var db database
if err := rows.Scan(&db.Name, &db.Owner, &db.Encoding); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
databases = append(databases, db)
}
if err = rows.Err(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
paged, total := controllers.Paginate(ctx, databases)
return controllers.Success(ctx, http.Json{
"total": total,
"items": paged,
})
}
// AddDatabase 添加数据库
func (r *PostgreSQLController) AddDatabase(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:40",
}); sanitize != nil {
return sanitize
}
database := ctx.Request().Input("database")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := shell.Execf(`echo "CREATE DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := shell.Execf(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := shell.Execf(`echo "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
if out, err := shell.Execf(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if err := systemctl.Reload("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
}
return controllers.Success(ctx, nil)
}
// DeleteDatabase 删除数据库
func (r *PostgreSQLController) DeleteDatabase(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
}); sanitize != nil {
return sanitize
}
database := ctx.Request().Input("database")
if out, err := shell.Execf(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
// BackupList 获取备份列表
func (r *PostgreSQLController) BackupList(ctx http.Context) http.Response {
backups, err := r.backup.PostgresqlList()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
}
paged, total := controllers.Paginate(ctx, backups)
return controllers.Success(ctx, http.Json{
"total": total,
"items": paged,
})
}
// UploadBackup 上传备份
func (r *PostgreSQLController) UploadBackup(ctx http.Context) http.Response {
file, err := ctx.Request().File("file")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
if !io.Exists(backupPath) {
if err = io.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
name := file.GetClientOriginalName()
_, err = file.StoreAs(backupPath, name)
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
}
return controllers.Success(ctx, nil)
}
// CreateBackup 创建备份
func (r *PostgreSQLController) CreateBackup(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
}); sanitize != nil {
return sanitize
}
database := ctx.Request().Input("database")
if err := r.backup.PostgresqlBackup(database); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// DeleteBackup 删除备份
func (r *PostgreSQLController) DeleteBackup(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"name": "required|min_len:1|max_len:255",
}); sanitize != nil {
return sanitize
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
fileName := ctx.Request().Input("name")
if err := io.Remove(backupPath + "/" + fileName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// RestoreBackup 还原备份
func (r *PostgreSQLController) RestoreBackup(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"backup": "required|min_len:1|max_len:255",
"database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
}); sanitize != nil {
return sanitize
}
if err := r.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup")); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
}
return controllers.Success(ctx, nil)
}
// RoleList 角色列表
func (r *PostgreSQLController) RoleList(ctx http.Context) http.Response {
type role struct {
Role string `json:"role"`
Attributes []string `json:"attributes"`
}
db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres dbname=postgres sslmode=disable")
if err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []role{},
})
}
if err = db.Ping(); err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []role{},
})
}
query := `
SELECT rolname,
rolsuper,
rolcreaterole,
rolcreatedb,
rolreplication,
rolbypassrls
FROM pg_roles
WHERE rolcanlogin = true;
`
rows, err := db.Query(query)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
defer rows.Close()
var roles []role
for rows.Next() {
var r role
var super, canCreateRole, canCreateDb, replication, bypassRls bool
if err = rows.Scan(&r.Role, &super, &canCreateRole, &canCreateDb, &replication, &bypassRls); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
permissions := map[string]bool{
"超级用户": super,
"创建角色": canCreateRole,
"创建数据库": canCreateDb,
"可以复制": replication,
"绕过行级安全": bypassRls,
}
for perm, enabled := range permissions {
if enabled {
r.Attributes = append(r.Attributes, perm)
}
}
if len(r.Attributes) == 0 {
r.Attributes = append(r.Attributes, "无")
}
roles = append(roles, r)
}
if err = rows.Err(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
paged, total := controllers.Paginate(ctx, roles)
return controllers.Success(ctx, http.Json{
"total": total,
"items": paged,
})
}
// AddRole 添加角色
func (r *PostgreSQLController) AddRole(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"database": "required|min_len:1|max_len:63|regex:^[a-zA-Z0-9_]+$",
"user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:40",
}); sanitize != nil {
return sanitize
}
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
database := ctx.Request().Input("database")
if out, err := shell.Execf(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := shell.Execf(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
if out, err := shell.Execf(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if err := systemctl.Reload("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
}
return controllers.Success(ctx, nil)
}
// DeleteRole 删除角色
func (r *PostgreSQLController) DeleteRole(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$",
}); sanitize != nil {
return sanitize
}
user := ctx.Request().Input("user")
if out, err := shell.Execf(`echo "DROP USER ` + user + `;" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := shell.Execf(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if err := systemctl.Reload("postgresql"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载服务失败")
}
return controllers.Success(ctx, nil)
}
// SetRolePassword 设置用户密码
func (r *PostgreSQLController) SetRolePassword(ctx http.Context) http.Response {
if sanitize := controllers.Sanitize(ctx, map[string]string{
"user": "required|min_len:1|max_len:30|regex:^[a-zA-Z0-9_]+$",
"password": "required|min_len:8|max_len:40",
}); sanitize != nil {
return sanitize
}
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if out, err := shell.Execf(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
return controllers.Success(ctx, nil)
}
@@ -7,119 +7,61 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/pkg/tools"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/os"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
"github.com/TheTNB/panel/pkg/types"
)
type PureFtpdController struct {
}
type User struct {
Username string `json:"username"`
Path string `json:"path"`
}
func NewPureFtpdController() *PureFtpdController {
return &PureFtpdController{}
}
// Status 获取运行状态
func (r *PureFtpdController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("pure-ftpd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
}
return controllers.Success(ctx, status)
}
// Restart 重启服务
func (r *PureFtpdController) Restart(ctx http.Context) http.Response {
err := tools.ServiceRestart("pure-ftpd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启PureFtpd失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *PureFtpdController) Start(ctx http.Context) http.Response {
err := tools.ServiceStart("pure-ftpd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动PureFtpd失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *PureFtpdController) Stop(ctx http.Context) http.Response {
err := tools.ServiceStop("pure-ftpd")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止PureFtpd失败")
}
return controllers.Success(ctx, nil)
}
// List 获取用户列表
func (r *PureFtpdController) List(ctx http.Context) http.Response {
listRaw, err := tools.Exec("pure-pw list")
listRaw, err := shell.Execf("pure-pw list")
if err != nil {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []User{},
"items": []types.PureFtpdUser{},
})
}
listArr := strings.Split(listRaw, "\n")
var users []User
var users []types.PureFtpdUser
for _, v := range listArr {
if len(v) == 0 {
continue
}
match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v)
users = append(users, User{
users = append(users, types.PureFtpdUser{
Username: match[1],
Path: strings.Replace(match[2], "/./", "/", 1),
})
}
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(users) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []User{},
})
}
if endIndex > len(users) {
endIndex = len(users)
}
pagedUsers := users[startIndex:endIndex]
paged, total := controllers.Paginate(ctx, users)
return controllers.Success(ctx, http.Json{
"total": len(users),
"items": pagedUsers,
"total": total,
"items": paged,
})
}
// Add 添加用户
func (r *PureFtpdController) Add(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
if sanitize := controllers.Sanitize(ctx, map[string]string{
"username": "required",
"password": "required|min_len:6",
"path": "required",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}); sanitize != nil {
return sanitize
}
username := ctx.Request().Input("username")
@@ -129,20 +71,20 @@ func (r *PureFtpdController) Add(ctx http.Context) http.Response {
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
if !tools.Exists(path) {
if !io.Exists(path) {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "目录不存在")
}
if err = tools.Chmod(path, 0755); err != nil {
if err := io.Chmod(path, 0755); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "修改目录权限失败")
}
if err = tools.Chown(path, "www", "www"); err != nil {
if err := io.Chown(path, "www", "www"); err != nil {
return nil
}
if out, err := tools.Exec(`yes '` + password + `' | pure-pw useradd ` + username + ` -u www -g www -d ` + path); err != nil {
if out, err := shell.Execf(`yes '` + password + `' | pure-pw useradd ` + username + ` -u www -g www -d ` + path); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("pure-pw mkdb"); err != nil {
if out, err := shell.Execf("pure-pw mkdb"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
@@ -151,22 +93,18 @@ func (r *PureFtpdController) Add(ctx http.Context) http.Response {
// Delete 删除用户
func (r *PureFtpdController) Delete(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
if sanitize := controllers.Sanitize(ctx, map[string]string{
"username": "required",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}); sanitize != nil {
return sanitize
}
username := ctx.Request().Input("username")
if out, err := tools.Exec("pure-pw userdel " + username + " -m"); err != nil {
if out, err := shell.Execf("pure-pw userdel " + username + " -m"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("pure-pw mkdb"); err != nil {
if out, err := shell.Execf("pure-pw mkdb"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
@@ -175,24 +113,20 @@ func (r *PureFtpdController) Delete(ctx http.Context) http.Response {
// ChangePassword 修改密码
func (r *PureFtpdController) ChangePassword(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
if sanitize := controllers.Sanitize(ctx, map[string]string{
"username": "required",
"password": "required|min_len:6",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}); sanitize != nil {
return sanitize
}
username := ctx.Request().Input("username")
password := ctx.Request().Input("password")
if out, err := tools.Exec(`yes '` + password + `' | pure-pw passwd ` + username + ` -m`); err != nil {
if out, err := shell.Execf(`yes '` + password + `' | pure-pw passwd ` + username + ` -m`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("pure-pw mkdb"); err != nil {
if out, err := shell.Execf("pure-pw mkdb"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
@@ -201,7 +135,7 @@ func (r *PureFtpdController) ChangePassword(ctx http.Context) http.Response {
// GetPort 获取端口
func (r *PureFtpdController) GetPort(ctx http.Context) http.Response {
port, err := tools.Exec(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
port, err := shell.Execf(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd端口失败")
}
@@ -211,35 +145,35 @@ func (r *PureFtpdController) GetPort(ctx http.Context) http.Response {
// SetPort 设置端口
func (r *PureFtpdController) SetPort(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
if sanitize := controllers.Sanitize(ctx, map[string]string{
"port": "required",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}); sanitize != nil {
return sanitize
}
port := ctx.Request().Input("port")
if out, err := tools.Exec(`sed -i "s/Bind.*/Bind 0.0.0.0,` + port + `/g" /www/server/pure-ftpd/etc/pure-ftpd.conf`); err != nil {
if out, err := shell.Execf(`sed -i "s/Bind.*/Bind 0.0.0.0,%s/g" /www/server/pure-ftpd/etc/pure-ftpd.conf`, port); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if tools.IsRHEL() {
if out, err := tools.Exec("firewall-cmd --zone=public --add-port=" + port + "/tcp --permanent"); err != nil {
if os.IsRHEL() {
if out, err := shell.Execf("firewall-cmd --zone=public --add-port=%s/tcp --permanent", port); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --reload"); err != nil {
if out, err := shell.Execf("firewall-cmd --reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := tools.Exec("ufw allow " + port + "/tcp"); err != nil {
if out, err := shell.Execf("ufw allow %s/tcp", port); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw reload"); err != nil {
if out, err := shell.Execf("ufw reload"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
}
return r.Restart(ctx)
if err := systemctl.Restart("pure-ftpd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
@@ -5,8 +5,11 @@ import (
"github.com/goravel/framework/contracts/http"
"panel/app/http/controllers"
"panel/pkg/tools"
"github.com/TheTNB/panel/app/http/controllers"
"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 RedisController struct {
@@ -16,47 +19,10 @@ func NewRedisController() *RedisController {
return &RedisController{}
}
// Status 获取运行状态
func (r *RedisController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("redis")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
}
return controllers.Success(ctx, status)
}
// Restart 重启服务
func (r *RedisController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("redis"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启Redis失败")
}
return controllers.Success(ctx, nil)
}
// Start 启动服务
func (r *RedisController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("redis"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动Redis失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止服务
func (r *RedisController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("redis"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止Redis失败")
}
return controllers.Success(ctx, nil)
}
// GetConfig 获取配置
func (r *RedisController) GetConfig(ctx http.Context) http.Response {
// 获取配置
config, err := tools.Read("/www/server/redis/redis.conf")
config, err := io.Read("/www/server/redis/redis.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis配置失败")
}
@@ -71,16 +37,20 @@ func (r *RedisController) SaveConfig(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
}
if err := tools.Write("/www/server/redis/redis.conf", config, 0644); err != nil {
if err := io.Write("/www/server/redis/redis.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入Redis配置失败")
}
return r.Restart(ctx)
if err := systemctl.Restart("redis"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启Redis失败")
}
return controllers.Success(ctx, nil)
}
// Load 获取负载
func (r *RedisController) Load(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("redis")
status, err := systemctl.Status("redis")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
}
@@ -88,7 +58,7 @@ func (r *RedisController) Load(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusInternalServerError, "Redis已停止运行")
}
raw, err := tools.Exec("redis-cli info")
raw, err := shell.Execf("redis-cli info")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis负载失败")
}
@@ -103,20 +73,20 @@ func (r *RedisController) Load(ctx http.Context) http.Response {
}
}
data := []LoadInfo{
{"TCP 端口", dataRaw["tcp_port"]},
{"已运行天数", dataRaw["uptime_in_days"]},
{"连接的客户端数", dataRaw["connected_clients"]},
{"已分配的内存总量", dataRaw["used_memory_human"]},
{"占用内存总量", dataRaw["used_memory_rss_human"]},
{"占用内存峰值", dataRaw["used_memory_peak_human"]},
{"内存碎片比率", dataRaw["mem_fragmentation_ratio"]},
{"运行以来连接过的客户端的总数", dataRaw["total_connections_received"]},
{"运行以来执行过的命令的总数", dataRaw["total_commands_processed"]},
{"每秒执行的命令数", dataRaw["instantaneous_ops_per_sec"]},
{"查找数据库键成功次数", dataRaw["keyspace_hits"]},
{"查找数据库键失败次数", dataRaw["keyspace_misses"]},
{"最近一次 fork() 操作耗费的毫秒数", dataRaw["latest_fork_usec"]},
data := []types.NV{
{Name: "TCP 端口", Value: dataRaw["tcp_port"]},
{Name: "已运行天数", Value: dataRaw["uptime_in_days"]},
{Name: "连接的客户端数", Value: dataRaw["connected_clients"]},
{Name: "已分配的内存总量", Value: dataRaw["used_memory_human"]},
{Name: "占用内存总量", Value: dataRaw["used_memory_rss_human"]},
{Name: "占用内存峰值", Value: dataRaw["used_memory_peak_human"]},
{Name: "内存碎片比率", Value: dataRaw["mem_fragmentation_ratio"]},
{Name: "运行以来连接过的客户端的总数", Value: dataRaw["total_connections_received"]},
{Name: "运行以来执行过的命令的总数", Value: dataRaw["total_commands_processed"]},
{Name: "每秒执行的命令数", Value: dataRaw["instantaneous_ops_per_sec"]},
{Name: "查找数据库键成功次数", Value: dataRaw["keyspace_hits"]},
{Name: "查找数据库键失败次数", Value: dataRaw["keyspace_misses"]},
{Name: "最近一次 fork() 操作耗费的毫秒数", Value: dataRaw["latest_fork_usec"]},
}
return controllers.Success(ctx, data)
@@ -0,0 +1,299 @@
package plugins
import (
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/app/http/controllers"
requests "github.com/TheTNB/panel/app/http/requests/plugins/rsync"
"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/types"
)
type RsyncController struct {
}
func NewRsyncController() *RsyncController {
return &RsyncController{}
}
// List
//
// @Summary 列出模块
// @Description 列出所有 Rsync 模块
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Param data query commonrequests.Paginate true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/modules [get]
func (r *RsyncController) List(ctx http.Context) http.Response {
config, err := io.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
var modules []types.RsyncModule
lines := strings.Split(config, "\n")
var currentModule *types.RsyncModule
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
if currentModule != nil {
modules = append(modules, *currentModule)
}
moduleName := line[1 : len(line)-1]
currentModule = &types.RsyncModule{
Name: moduleName,
}
} else if currentModule != nil {
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
switch key {
case "path":
currentModule.Path = value
case "comment":
currentModule.Comment = value
case "read only":
currentModule.ReadOnly = value == "yes" || value == "true"
case "auth users":
currentModule.AuthUser = value
currentModule.Secret, err = shell.Execf("grep -E '^" + currentModule.AuthUser + ":.*$' /etc/rsyncd.secrets | awk -F ':' '{print $2}'")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取模块"+currentModule.AuthUser+"的密钥失败")
}
case "hosts allow":
currentModule.HostsAllow = value
}
}
}
}
if currentModule != nil {
modules = append(modules, *currentModule)
}
paged, total := controllers.Paginate(ctx, modules)
return controllers.Success(ctx, http.Json{
"total": total,
"items": paged,
})
}
// Create
//
// @Summary 添加模块
// @Description 添加 Rsync 模块
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Param data body requests.Create true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/modules [post]
func (r *RsyncController) Create(ctx http.Context) http.Response {
var createRequest requests.Create
sanitize := controllers.SanitizeRequest(ctx, &createRequest)
if sanitize != nil {
return sanitize
}
config, err := io.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if strings.Contains(config, "["+createRequest.Name+"]") {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+createRequest.Name+" 已存在")
}
conf := `# ` + createRequest.Name + `-START
[` + createRequest.Name + `]
path = ` + createRequest.Path + `
comment = ` + createRequest.Comment + `
read only = no
auth users = ` + createRequest.AuthUser + `
hosts allow = ` + createRequest.HostsAllow + `
secrets file = /etc/rsyncd.secrets
# ` + createRequest.Name + `-END
`
if err := io.WriteAppend("/etc/rsyncd.conf", conf); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("echo '" + createRequest.AuthUser + ":" + createRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if err := systemctl.Restart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// Destroy
//
// @Summary 删除模块
// @Description 删除 Rsync 模块
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Param name path string true "模块名称"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/modules/{name} [delete]
func (r *RsyncController) Destroy(ctx http.Context) http.Response {
name := ctx.Request().Input("name")
if len(name) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "name 不能为空")
}
config, err := io.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if !strings.Contains(config, "["+name+"]") {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+name+" 不存在")
}
module := str.Cut(config, "# "+name+"-START", "# "+name+"-END")
config = strings.Replace(config, "\n# "+name+"-START"+module+"# "+name+"-END", "", -1)
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
if len(match) == 2 {
authUser := match[1]
if out, err := shell.Execf("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
}
if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = systemctl.Restart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// Update
//
// @Summary 更新模块
// @Description 更新 Rsync 模块
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Param name path string true "模块名称"
// @Param data body requests.Update true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/modules/{name} [post]
func (r *RsyncController) Update(ctx http.Context) http.Response {
var updateRequest requests.Update
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
config, err := io.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if !strings.Contains(config, "["+updateRequest.Name+"]") {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "模块 "+updateRequest.Name+" 不存在")
}
newConf := `# ` + updateRequest.Name + `-START
[` + updateRequest.Name + `]
path = ` + updateRequest.Path + `
comment = ` + updateRequest.Comment + `
read only = no
auth users = ` + updateRequest.AuthUser + `
hosts allow = ` + updateRequest.HostsAllow + `
secrets file = /etc/rsyncd.secrets
# ` + updateRequest.Name + `-END`
module := str.Cut(config, "# "+updateRequest.Name+"-START", "# "+updateRequest.Name+"-END")
config = strings.Replace(config, "# "+updateRequest.Name+"-START"+module+"# "+updateRequest.Name+"-END", newConf, -1)
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
if len(match) == 2 {
authUser := match[1]
if out, err := shell.Execf("sed -i '/^" + authUser + ":.*$/d' /etc/rsyncd.secrets"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
}
if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := shell.Execf("echo '" + updateRequest.AuthUser + ":" + updateRequest.Secret + "' >> /etc/rsyncd.secrets"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if err = systemctl.Restart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
// GetConfig
//
// @Summary 获取配置
// @Description 获取 Rsync 配置
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/config [get]
func (r *RsyncController) GetConfig(ctx http.Context) http.Response {
config, err := io.Read("/etc/rsyncd.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
// UpdateConfig
//
// @Summary 更新配置
// @Description 更新 Rsync 配置
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Param data body requests.UpdateConfig true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /plugins/rsync/config [post]
func (r *RsyncController) UpdateConfig(ctx http.Context) http.Response {
var updateRequest requests.UpdateConfig
sanitize := controllers.SanitizeRequest(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
if err := io.Write("/etc/rsyncd.conf", updateRequest.Config, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := systemctl.Restart("rsyncd"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
+34 -53
View File
@@ -8,13 +8,16 @@ import (
"github.com/goravel/framework/support/json"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/services"
"panel/pkg/tools"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/types"
)
type S3fsController struct {
setting services.Setting
setting internal.Setting
}
func NewS3fsController() *S3fsController {
@@ -25,51 +28,30 @@ func NewS3fsController() *S3fsController {
// List 所有 S3fs 挂载
func (r *S3fsController) List(ctx http.Context) http.Response {
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
var s3fsList []S3fsMount
var s3fsList []types.S3fsMount
err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(s3fsList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []S3fsMount{},
})
}
if endIndex > len(s3fsList) {
endIndex = len(s3fsList)
}
pagedS3fsList := s3fsList[startIndex:endIndex]
if pagedS3fsList == nil {
pagedS3fsList = []S3fsMount{}
}
paged, total := controllers.Paginate(ctx, s3fsList)
return controllers.Success(ctx, http.Json{
"total": len(s3fsList),
"items": pagedS3fsList,
"total": total,
"items": paged,
})
}
// Add 添加 S3fs 挂载
func (r *S3fsController) Add(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
if sanitize := controllers.Sanitize(ctx, map[string]string{
"ak": "required|regex:^[a-zA-Z0-9]*$",
"sk": "required|regex:^[a-zA-Z0-9]*$",
"bucket": "required|regex:^[a-zA-Z0-9_-]*$",
"url": "required|full_url",
"path": "required|regex:^/[a-zA-Z0-9_-]+$",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}); sanitize != nil {
return sanitize
}
ak := ctx.Request().Input("ak")
@@ -84,18 +66,17 @@ func (r *S3fsController) Add(ctx http.Context) http.Response {
}
// 检查挂载目录是否存在且为空
if !tools.Exists(path) {
if err = tools.Mkdir(path, 0755); err != nil {
if !io.Exists(path) {
if err := io.Mkdir(path, 0755); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录创建失败")
}
}
if !tools.Empty(path) {
if !io.Empty(path) {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空")
}
var s3fsList []S3fsMount
err = json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
var s3fsList []types.S3fsMount
if err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
}
@@ -107,23 +88,23 @@ func (r *S3fsController) Add(ctx http.Context) http.Response {
id := carbon.Now().TimestampMilli()
password := ak + ":" + sk
if err = tools.Write("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600); err != nil {
if err := io.Write("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600); err != nil {
return nil
}
out, err := tools.Exec(`echo 's3fs#` + bucket + ` ` + path + ` fuse _netdev,allow_other,nonempty,url=` + url + `,passwd_file=/etc/passwd-s3fs-` + cast.ToString(id) + ` 0 0' >> /etc/fstab`)
out, err := shell.Execf(`echo 's3fs#` + bucket + ` ` + path + ` fuse _netdev,allow_other,nonempty,url=` + url + `,passwd_file=/etc/passwd-s3fs-` + cast.ToString(id) + ` 0 0' >> /etc/fstab`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if mountCheck, err := tools.Exec("mount -a 2>&1"); err != nil {
_, _ = tools.Exec(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
if mountCheck, err := shell.Execf("mount -a 2>&1"); err != nil {
_, _ = shell.Execf(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
return controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck)
}
if _, err := tools.Exec("df -h | grep " + path + " 2>&1"); err != nil {
_, _ = tools.Exec(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
if _, err := shell.Execf("df -h | grep " + path + " 2>&1"); err != nil {
_, _ = shell.Execf(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
return controllers.Error(ctx, http.StatusInternalServerError, "挂载失败,请检查配置是否正确")
}
s3fsList = append(s3fsList, S3fsMount{
s3fsList = append(s3fsList, types.S3fsMount{
ID: id,
Path: path,
Bucket: bucket,
@@ -148,13 +129,13 @@ func (r *S3fsController) Delete(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不能为空")
}
var s3fsList []S3fsMount
var s3fsList []types.S3fsMount
err := json.UnmarshalString(r.setting.Get("s3fs", "[]"), &s3fsList)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
}
var mount S3fsMount
var mount types.S3fsMount
for _, s := range s3fsList {
if cast.ToString(s.ID) == id {
mount = s
@@ -165,23 +146,23 @@ func (r *S3fsController) Delete(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不存在")
}
if out, err := tools.Exec(`fusermount -u '` + mount.Path + `' 2>&1`); err != nil {
if out, err := shell.Execf(`fusermount -u '` + mount.Path + `' 2>&1`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`umount '` + mount.Path + `' 2>&1`); err != nil {
if out, err := shell.Execf(`umount '` + mount.Path + `' 2>&1`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`); err != nil {
if out, err := shell.Execf(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if mountCheck, err := tools.Exec("mount -a 2>&1"); err != nil {
if mountCheck, err := shell.Execf("mount -a 2>&1"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck)
}
if err := tools.Remove("/etc/passwd-s3fs-" + cast.ToString(mount.ID)); err != nil {
if err := io.Remove("/etc/passwd-s3fs-" + cast.ToString(mount.ID)); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
var newS3fsList []S3fsMount
var newS3fsList []types.S3fsMount
for _, s := range s3fsList {
if s.ID != mount.ID {
newS3fsList = append(newS3fsList, s)
@@ -1,81 +1,44 @@
package plugins
import (
"fmt"
"strconv"
"strings"
"github.com/goravel/framework/contracts/http"
"panel/pkg/tools"
"panel/app/http/controllers"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/os"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
)
type SupervisorController struct {
ServiceName string
service string
}
func NewSupervisorController() *SupervisorController {
var serviceName string
if tools.IsRHEL() {
serviceName = "supervisord"
var service string
if os.IsRHEL() {
service = "supervisord"
} else {
serviceName = "supervisor"
service = "supervisor"
}
return &SupervisorController{
ServiceName: serviceName,
service: service,
}
}
// Status 状态
func (r *SupervisorController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus(r.ServiceName)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取Supervisor状态失败")
}
return controllers.Success(ctx, status)
}
// Start 启动
func (r *SupervisorController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart(r.ServiceName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动Supervisor失败")
}
return controllers.Success(ctx, nil)
}
// Stop 停止
func (r *SupervisorController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop(r.ServiceName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止Supervisor失败")
}
return controllers.Success(ctx, nil)
}
// Restart 重启
func (r *SupervisorController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart(r.ServiceName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启Supervisor失败")
}
return controllers.Success(ctx, nil)
}
// Reload 重载
func (r *SupervisorController) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload(r.ServiceName); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载Supervisor失败")
}
return controllers.Success(ctx, nil)
// Service 获取服务名称
func (r *SupervisorController) Service(ctx http.Context) http.Response {
return controllers.Success(ctx, r.service)
}
// Log 日志
func (r *SupervisorController) Log(ctx http.Context) http.Response {
log, err := tools.Exec(`tail -n 200 /var/log/supervisor/supervisord.log`)
log, err := shell.Execf(`tail -n 200 /var/log/supervisor/supervisord.log`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
@@ -85,7 +48,7 @@ func (r *SupervisorController) Log(ctx http.Context) http.Response {
// ClearLog 清空日志
func (r *SupervisorController) ClearLog(ctx http.Context) http.Response {
if out, err := tools.Exec(`echo "" > /var/log/supervisor/supervisord.log`); err != nil {
if out, err := shell.Execf(`echo "" > /var/log/supervisor/supervisord.log`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
@@ -96,10 +59,10 @@ func (r *SupervisorController) ClearLog(ctx http.Context) http.Response {
func (r *SupervisorController) Config(ctx http.Context) http.Response {
var config string
var err error
if tools.IsRHEL() {
config, err = tools.Read(`/etc/supervisord.conf`)
if os.IsRHEL() {
config, err = io.Read(`/etc/supervisord.conf`)
} else {
config, err = tools.Read(`/etc/supervisor/supervisord.conf`)
config, err = io.Read(`/etc/supervisor/supervisord.conf`)
}
if err != nil {
@@ -113,24 +76,25 @@ func (r *SupervisorController) Config(ctx http.Context) http.Response {
func (r *SupervisorController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
var err error
if tools.IsRHEL() {
err = tools.Write(`/etc/supervisord.conf`, config, 0644)
if os.IsRHEL() {
err = io.Write(`/etc/supervisord.conf`, config, 0644)
} else {
err = tools.Write(`/etc/supervisor/supervisord.conf`, config, 0644)
err = io.Write(`/etc/supervisor/supervisord.conf`, config, 0644)
}
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return r.Restart(ctx)
if err = systemctl.Restart(r.service); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", r.service))
}
return controllers.Success(ctx, nil)
}
// Processes 进程列表
func (r *SupervisorController) Processes(ctx http.Context) http.Response {
page := ctx.Request().QueryInt("page", 1)
limit := ctx.Request().QueryInt("limit", 10)
type process struct {
Name string `json:"name"`
Status string `json:"status"`
@@ -138,12 +102,12 @@ func (r *SupervisorController) Processes(ctx http.Context) http.Response {
Uptime string `json:"uptime"`
}
out, err := tools.Exec(`supervisorctl status | awk '{print $1}'`)
out, err := shell.Execf(`supervisorctl status | awk '{print $1}'`)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
var processList []process
var processes []process
for _, line := range strings.Split(out, "\n") {
if len(line) == 0 {
continue
@@ -151,47 +115,33 @@ func (r *SupervisorController) Processes(ctx http.Context) http.Response {
var p process
p.Name = line
if status, err := tools.Exec(`supervisorctl status ` + line + ` | awk '{print $2}'`); err == nil {
if status, err := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $2}'`); err == nil {
p.Status = status
}
if p.Status == "RUNNING" {
pid, _ := tools.Exec(`supervisorctl status ` + line + ` | awk '{print $4}'`)
pid, _ := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $4}'`)
p.Pid = strings.ReplaceAll(pid, ",", "")
uptime, _ := tools.Exec(`supervisorctl status ` + line + ` | awk '{print $6}'`)
uptime, _ := shell.Execf(`supervisorctl status ` + line + ` | awk '{print $6}'`)
p.Uptime = uptime
} else {
p.Pid = "-"
p.Uptime = "-"
}
processList = append(processList, p)
processes = append(processes, p)
}
startIndex := (page - 1) * limit
endIndex := page * limit
if startIndex > len(processList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []process{},
})
}
if endIndex > len(processList) {
endIndex = len(processList)
}
pagedProcessList := processList[startIndex:endIndex]
if pagedProcessList == nil {
pagedProcessList = []process{}
}
paged, total := controllers.Paginate(ctx, processes)
return controllers.Success(ctx, http.Json{
"total": len(processList),
"items": pagedProcessList,
"total": total,
"items": paged,
})
}
// StartProcess 启动进程
func (r *SupervisorController) StartProcess(ctx http.Context) http.Response {
process := ctx.Request().Input("process")
if out, err := tools.Exec(`supervisorctl start ` + process); err != nil {
if out, err := shell.Execf(`supervisorctl start %s`, process); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
@@ -201,7 +151,7 @@ func (r *SupervisorController) StartProcess(ctx http.Context) http.Response {
// StopProcess 停止进程
func (r *SupervisorController) StopProcess(ctx http.Context) http.Response {
process := ctx.Request().Input("process")
if out, err := tools.Exec(`supervisorctl stop ` + process); err != nil {
if out, err := shell.Execf(`supervisorctl stop %s`, process); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
@@ -211,7 +161,7 @@ func (r *SupervisorController) StopProcess(ctx http.Context) http.Response {
// RestartProcess 重启进程
func (r *SupervisorController) RestartProcess(ctx http.Context) http.Response {
process := ctx.Request().Input("process")
if out, err := tools.Exec(`supervisorctl restart ` + process); err != nil {
if out, err := shell.Execf(`supervisorctl restart %s`, process); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
@@ -223,17 +173,17 @@ func (r *SupervisorController) ProcessLog(ctx http.Context) http.Response {
process := ctx.Request().Input("process")
var logPath string
var err error
if tools.IsRHEL() {
logPath, err = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
if os.IsRHEL() {
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
} else {
logPath, err = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
}
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径")
}
log, err := tools.Exec(`tail -n 200 ` + logPath)
log, err := shell.Execf(`tail -n 200 ` + logPath)
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, log)
}
@@ -246,17 +196,17 @@ func (r *SupervisorController) ClearProcessLog(ctx http.Context) http.Response {
process := ctx.Request().Input("process")
var logPath string
var err error
if tools.IsRHEL() {
logPath, err = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
if os.IsRHEL() {
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
} else {
logPath, err = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
}
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径")
return controllers.Error(ctx, http.StatusInternalServerError, fmt.Sprintf("无法从进程%s的配置文件中获取日志路径", process))
}
if out, err := tools.Exec(`echo "" > ` + logPath); err != nil {
if out, err := shell.Execf(`echo "" > ` + logPath); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
@@ -268,10 +218,10 @@ func (r *SupervisorController) ProcessConfig(ctx http.Context) http.Response {
process := ctx.Request().Query("process")
var config string
var err error
if tools.IsRHEL() {
config, err = tools.Read(`/etc/supervisord.d/` + process + `.conf`)
if os.IsRHEL() {
config, err = io.Read(`/etc/supervisord.d/` + process + `.conf`)
} else {
config, err = tools.Read(`/etc/supervisor/conf.d/` + process + `.conf`)
config, err = io.Read(`/etc/supervisor/conf.d/` + process + `.conf`)
}
if err != nil {
@@ -286,43 +236,33 @@ func (r *SupervisorController) SaveProcessConfig(ctx http.Context) http.Response
process := ctx.Request().Input("process")
config := ctx.Request().Input("config")
var err error
if tools.IsRHEL() {
err = tools.Write(`/etc/supervisord.d/`+process+`.conf`, config, 0644)
if os.IsRHEL() {
err = io.Write(`/etc/supervisord.d/`+process+`.conf`, config, 0644)
} else {
err = tools.Write(`/etc/supervisor/conf.d/`+process+`.conf`, config, 0644)
err = io.Write(`/etc/supervisor/conf.d/`+process+`.conf`, config, 0644)
}
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if out, err := tools.Exec(`supervisorctl reread`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`supervisorctl update`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`supervisorctl start ` + process); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
_, _ = shell.Execf(`supervisorctl reread`)
_, _ = shell.Execf(`supervisorctl update`)
_, _ = shell.Execf(`supervisorctl restart %s`, process)
return controllers.Success(ctx, nil)
}
// AddProcess 添加进程
func (r *SupervisorController) AddProcess(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
if sanitize := controllers.Sanitize(ctx, map[string]string{
"name": "required|alpha_dash",
"user": "required|alpha_dash",
"path": "required",
"command": "required",
"num": "required",
})
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}); sanitize != nil {
return sanitize
}
name := ctx.Request().Input("name")
@@ -342,25 +282,21 @@ redirect_stderr=true
stdout_logfile=/var/log/supervisor/` + name + `.log
stdout_logfile_maxbytes=2MB
`
if tools.IsRHEL() {
err = tools.Write(`/etc/supervisord.d/`+name+`.conf`, config, 0644)
var err error
if os.IsRHEL() {
err = io.Write(`/etc/supervisord.d/`+name+`.conf`, config, 0644)
} else {
err = tools.Write(`/etc/supervisor/conf.d/`+name+`.conf`, config, 0644)
err = io.Write(`/etc/supervisor/conf.d/`+name+`.conf`, config, 0644)
}
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if out, err := tools.Exec(`supervisorctl reread`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`supervisorctl update`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`supervisorctl start ` + name); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
_, _ = shell.Execf(`supervisorctl reread`)
_, _ = shell.Execf(`supervisorctl update`)
_, _ = shell.Execf(`supervisorctl start %s`, name)
return controllers.Success(ctx, nil)
}
@@ -368,20 +304,20 @@ stdout_logfile_maxbytes=2MB
// DeleteProcess 删除进程
func (r *SupervisorController) DeleteProcess(ctx http.Context) http.Response {
process := ctx.Request().Input("process")
if out, err := tools.Exec(`supervisorctl stop ` + process); err != nil {
if out, err := shell.Execf(`supervisorctl stop %s`, process); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
var logPath string
var err error
if tools.IsRHEL() {
logPath, err = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
if err := tools.Remove(`/etc/supervisord.d/` + process + `.conf`); err != nil {
if os.IsRHEL() {
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
if err := io.Remove(`/etc/supervisord.d/` + process + `.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
} else {
logPath, err = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
if err := tools.Remove(`/etc/supervisor/conf.d/` + process + `.conf`); err != nil {
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, process)
if err := io.Remove(`/etc/supervisor/conf.d/` + process + `.conf`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
}
@@ -390,15 +326,11 @@ func (r *SupervisorController) DeleteProcess(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusInternalServerError, "无法从进程"+process+"的配置文件中获取日志路径")
}
if err := tools.Remove(logPath); err != nil {
if err := io.Remove(logPath); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
if out, err := tools.Exec(`supervisorctl reread`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec(`supervisorctl update`); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, out)
}
_, _ = shell.Execf(`supervisorctl reread`)
_, _ = shell.Execf(`supervisorctl update`)
return controllers.Success(ctx, nil)
}
@@ -7,8 +7,10 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/pkg/tools"
"github.com/TheTNB/panel/app/http/controllers"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/str"
)
type ToolBoxController struct {
@@ -20,7 +22,7 @@ func NewToolBoxController() *ToolBoxController {
// GetDNS 获取 DNS 信息
func (r *ToolBoxController) GetDNS(ctx http.Context) http.Response {
raw, err := tools.Read("/etc/resolv.conf")
raw, err := io.Read("/etc/resolv.conf")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
@@ -50,7 +52,7 @@ func (r *ToolBoxController) SetDNS(ctx http.Context) http.Response {
dns += "nameserver " + dns1 + "\n"
dns += "nameserver " + dns2 + "\n"
if err := tools.Write("/etc/resolv.conf", dns, 0644); err != nil {
if err := io.Write("/etc/resolv.conf", dns, 0644); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "写入 DNS 信息失败")
}
@@ -61,28 +63,28 @@ func (r *ToolBoxController) SetDNS(ctx http.Context) http.Response {
func (r *ToolBoxController) GetSWAP(ctx http.Context) http.Response {
var total, used, free string
var size int64
if tools.Exists("/www/swap") {
file, err := tools.FileInfo("/www/swap")
if io.Exists("/www/swap") {
file, err := io.FileInfo("/www/swap")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
}
size = file.Size() / 1024 / 1024
total = tools.FormatBytes(float64(file.Size()))
total = str.FormatBytes(float64(file.Size()))
} else {
size = 0
total = "0.00 B"
}
raw, err := tools.Exec("free | grep Swap")
raw, err := shell.Execf("free | grep Swap")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取 SWAP 信息失败")
}
match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
if len(match) > 0 {
used = tools.FormatBytes(cast.ToFloat64(match[2]) * 1024)
free = tools.FormatBytes(cast.ToFloat64(match[3]) * 1024)
used = str.FormatBytes(cast.ToFloat64(match[2]) * 1024)
free = str.FormatBytes(cast.ToFloat64(match[3]) * 1024)
}
return controllers.Success(ctx, http.Json{
@@ -97,47 +99,47 @@ func (r *ToolBoxController) GetSWAP(ctx http.Context) http.Response {
func (r *ToolBoxController) SetSWAP(ctx http.Context) http.Response {
size := ctx.Request().InputInt("size")
if tools.Exists("/www/swap") {
if out, err := tools.Exec("swapoff /www/swap"); err != nil {
if io.Exists("/www/swap") {
if out, err := shell.Execf("swapoff /www/swap"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
if out, err := tools.Exec("rm -f /www/swap"); err != nil {
if out, err := shell.Execf("rm -f /www/swap"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
if out, err := tools.Exec("sed -i '/www\\/swap/d' /etc/fstab"); err != nil {
if out, err := shell.Execf("sed -i '/www\\/swap/d' /etc/fstab"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
}
if size > 1 {
free, err := tools.Exec("df -k /www | awk '{print $4}' | tail -n 1")
free, err := shell.Execf("df -k /www | awk '{print $4}' | tail -n 1")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取磁盘空间失败")
}
if cast.ToInt64(free)*1024 < int64(size)*1024*1024 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "磁盘空间不足,当前剩余 "+tools.FormatBytes(cast.ToFloat64(free)))
return controllers.Error(ctx, http.StatusUnprocessableEntity, "磁盘空间不足,当前剩余 "+str.FormatBytes(cast.ToFloat64(free)))
}
btrfsCheck, _ := tools.Exec("df -T /www | awk '{print $2}' | tail -n 1")
btrfsCheck, _ := shell.Execf("df -T /www | awk '{print $2}' | tail -n 1")
if strings.Contains(btrfsCheck, "btrfs") {
if out, err := tools.Exec("btrfs filesystem mkswapfile --size " + cast.ToString(size) + "M --uuid clear /www/swap"); err != nil {
if out, err := shell.Execf("btrfs filesystem mkswapfile --size " + cast.ToString(size) + "M --uuid clear /www/swap"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
} else {
if out, err := tools.Exec("dd if=/dev/zero of=/www/swap bs=1M count=" + cast.ToString(size)); err != nil {
if out, err := shell.Execf("dd if=/dev/zero of=/www/swap bs=1M count=" + cast.ToString(size)); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
if out, err := tools.Exec("mkswap -f /www/swap"); err != nil {
if out, err := shell.Execf("mkswap -f /www/swap"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
if err := tools.Chmod("/www/swap", 0600); err != nil {
if err := io.Chmod("/www/swap", 0600); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "设置 SWAP 权限失败")
}
}
if out, err := tools.Exec("swapon /www/swap"); err != nil {
if out, err := shell.Execf("swapon /www/swap"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
if out, err := tools.Exec("echo '/www/swap swap swap defaults 0 0' >> /etc/fstab"); err != nil {
if out, err := shell.Execf("echo '/www/swap swap swap defaults 0 0' >> /etc/fstab"); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
}
@@ -147,7 +149,7 @@ func (r *ToolBoxController) SetSWAP(ctx http.Context) http.Response {
// GetTimezone 获取时区
func (r *ToolBoxController) GetTimezone(ctx http.Context) http.Response {
raw, err := tools.Exec("timedatectl | grep zone")
raw, err := shell.Execf("timedatectl | grep zone")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取时区信息失败")
}
@@ -162,7 +164,7 @@ func (r *ToolBoxController) GetTimezone(ctx http.Context) http.Response {
Value string `json:"value"`
}
zonesRaw, err := tools.Exec("timedatectl list-timezones")
zonesRaw, err := shell.Execf("timedatectl list-timezones")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取时区列表失败")
}
@@ -189,7 +191,7 @@ func (r *ToolBoxController) SetTimezone(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "时区不能为空")
}
if out, err := tools.Exec("timedatectl set-timezone " + timezone); err != nil {
if out, err := shell.Execf("timedatectl set-timezone %s", timezone); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
@@ -198,7 +200,7 @@ func (r *ToolBoxController) SetTimezone(ctx http.Context) http.Response {
// GetHosts 获取 hosts 信息
func (r *ToolBoxController) GetHosts(ctx http.Context) http.Response {
hosts, err := tools.Read("/etc/hosts")
hosts, err := io.Read("/etc/hosts")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
@@ -213,7 +215,7 @@ func (r *ToolBoxController) SetHosts(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "hosts 信息不能为空")
}
if err := tools.Write("/etc/hosts", hosts, 0644); err != nil {
if err := io.Write("/etc/hosts", hosts, 0644); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "写入 hosts 信息失败")
}
@@ -231,7 +233,7 @@ func (r *ToolBoxController) SetRootPassword(ctx http.Context) http.Response {
}
password = strings.ReplaceAll(password, `'`, `\'`)
if out, err := tools.Exec(`yes '` + password + `' | passwd root`); err != nil {
if out, err := shell.Execf(`yes '` + password + `' | passwd root`); err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, out)
}
+94 -127
View File
@@ -6,18 +6,27 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/spf13/cast"
commonrequests "panel/app/http/requests/common"
"panel/pkg/tools"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/os"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/systemctl"
)
type SafeController struct {
// Dependent services
ssh string
}
func NewSafeController() *SafeController {
var ssh string
if os.IsRHEL() {
ssh = "sshd"
} else {
ssh = "ssh"
}
return &SafeController{
// Inject services
ssh: ssh,
}
}
@@ -28,24 +37,41 @@ func (r *SafeController) GetFirewallStatus(ctx http.Context) http.Response {
// SetFirewallStatus 设置防火墙状态
func (r *SafeController) SetFirewallStatus(ctx http.Context) http.Response {
var out string
var err error
if ctx.Request().InputBool("status") {
if tools.IsRHEL() {
out, err = tools.Exec("systemctl start firewalld")
if os.IsRHEL() {
err = systemctl.Start("firewalld")
if err == nil {
err = systemctl.Enable("firewalld")
}
} else {
out, err = tools.Exec("echo y | ufw enable")
_, err = shell.Execf("echo y | ufw enable")
if err == nil {
err = systemctl.Start("ufw")
}
if err == nil {
err = systemctl.Enable("ufw")
}
}
} else {
if tools.IsRHEL() {
out, err = tools.Exec("systemctl stop firewalld")
if os.IsRHEL() {
err = systemctl.Stop("firewalld")
if err == nil {
err = systemctl.Disable("firewalld")
}
} else {
out, err = tools.Exec("ufw disable")
_, err = shell.Execf("ufw disable")
if err == nil {
err = systemctl.Stop("ufw")
}
if err == nil {
err = systemctl.Disable("ufw")
}
}
}
if err != nil {
return Error(ctx, http.StatusInternalServerError, out)
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
@@ -53,19 +79,13 @@ func (r *SafeController) SetFirewallStatus(ctx http.Context) http.Response {
// GetFirewallRules 获取防火墙规则
func (r *SafeController) GetFirewallRules(ctx http.Context) http.Response {
var paginateRequest commonrequests.Paginate
sanitize := Sanitize(ctx, &paginateRequest)
if sanitize != nil {
return sanitize
}
if !r.firewallStatus() {
return Success(ctx, nil)
}
var rules []map[string]string
if tools.IsRHEL() {
out, err := tools.Exec("firewall-cmd --list-all 2>&1")
if os.IsRHEL() {
out, err := shell.Execf("firewall-cmd --list-all")
if err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
@@ -86,7 +106,7 @@ func (r *SafeController) GetFirewallRules(ctx http.Context) http.Response {
})
}
} else {
out, err := tools.Exec("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'")
out, err := shell.Execf("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'")
if err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
@@ -106,22 +126,11 @@ func (r *SafeController) GetFirewallRules(ctx http.Context) http.Response {
}
}
startIndex := (paginateRequest.Page - 1) * paginateRequest.Limit
endIndex := paginateRequest.Page * paginateRequest.Limit
if startIndex > len(rules) {
return Success(ctx, http.Json{
"total": 0,
"items": []map[string]string{},
})
}
if endIndex > len(rules) {
endIndex = len(rules)
}
pagedRules := rules[startIndex:endIndex]
paged, total := Paginate(ctx, rules)
return Success(ctx, http.Json{
"total": len(rules),
"items": pagedRules,
"total": total,
"items": paged,
})
}
@@ -151,14 +160,14 @@ func (r *SafeController) AddFirewallRule(ctx http.Context) http.Response {
}
}
if tools.IsRHEL() {
if out, err := tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1"); err != nil {
if os.IsRHEL() {
if out, err := shell.Execf("firewall-cmd --remove-port=%s/%s --permanent", port, protocol); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --add-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1"); err != nil {
if out, err := shell.Execf("firewall-cmd --add-port=%s/%s --permanent", port, protocol); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --reload"); err != nil {
if out, err := shell.Execf("firewall-cmd --reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
@@ -166,13 +175,13 @@ func (r *SafeController) AddFirewallRule(ctx http.Context) http.Response {
if strings.Contains(port, "-") {
port = strings.ReplaceAll(port, "-", ":")
}
if out, err := tools.Exec("ufw delete allow " + cast.ToString(port) + "/" + protocol); err != nil {
if out, err := shell.Execf("ufw delete allow %s/%s", port, protocol); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw allow " + cast.ToString(port) + "/" + protocol); err != nil {
if out, err := shell.Execf("ufw allow %s/%s", port, protocol); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw reload"); err != nil {
if out, err := shell.Execf("ufw reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
@@ -186,24 +195,24 @@ func (r *SafeController) DeleteFirewallRule(ctx http.Context) http.Response {
return Error(ctx, http.StatusUnprocessableEntity, "防火墙未启动")
}
port := ctx.Request().InputInt("port", 0)
protocol := ctx.Request().Input("protocol", "")
if port == 0 || protocol == "" {
port := ctx.Request().Input("port")
protocol := ctx.Request().Input("protocol")
if port == "" || protocol == "" {
return Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
if tools.IsRHEL() {
if out, err := tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1"); err != nil {
if os.IsRHEL() {
if out, err := shell.Execf("firewall-cmd --remove-port=%s/%s --permanent", port, protocol); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --reload"); err != nil {
if out, err := shell.Execf("firewall-cmd --reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := tools.Exec("ufw delete allow " + cast.ToString(port) + "/" + protocol); err != nil {
if out, err := shell.Execf("ufw delete allow %s/%s", port, protocol); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw reload"); err != nil {
if out, err := shell.Execf("ufw reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
@@ -213,27 +222,11 @@ func (r *SafeController) DeleteFirewallRule(ctx http.Context) http.Response {
// firewallStatus 获取防火墙状态
func (r *SafeController) firewallStatus() bool {
var out string
var err error
var running bool
if tools.IsRHEL() {
out, err = tools.Exec("systemctl status firewalld | grep Active | awk '{print $3}'")
if out == "(running)" {
running = true
} else {
running = false
}
if os.IsRHEL() {
running, _ = systemctl.Status("firewalld")
} else {
out, err = tools.Exec("ufw status | grep Status | awk '{print $2}'")
if out == "active" {
running = true
} else {
running = false
}
}
if err != nil {
return false
running, _ = systemctl.Status("ufw")
}
return running
@@ -241,54 +234,29 @@ func (r *SafeController) firewallStatus() bool {
// GetSshStatus 获取 SSH 状态
func (r *SafeController) GetSshStatus(ctx http.Context) http.Response {
var out string
var err error
if tools.IsRHEL() {
out, err = tools.Exec("systemctl status sshd | grep Active | awk '{print $3}'")
} else {
out, err = tools.Exec("systemctl status ssh | grep Active | awk '{print $3}'")
}
running, err := systemctl.Status(r.ssh)
if err != nil {
return Error(ctx, http.StatusInternalServerError, out)
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, out == "(running)")
return Success(ctx, running)
}
// SetSshStatus 设置 SSH 状态
func (r *SafeController) SetSshStatus(ctx http.Context) http.Response {
if ctx.Request().InputBool("status") {
if tools.IsRHEL() {
if out, err := tools.Exec("systemctl enable sshd"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("systemctl start sshd"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := tools.Exec("systemctl enable ssh"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("systemctl start ssh"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if err := systemctl.Enable(r.ssh); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := systemctl.Start(r.ssh); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
} else {
if tools.IsRHEL() {
if out, err := tools.Exec("systemctl stop sshd"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("systemctl disable sshd"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := tools.Exec("systemctl stop ssh"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("systemctl disable ssh"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if err := systemctl.Stop(r.ssh); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := systemctl.Disable(r.ssh); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
}
@@ -297,12 +265,12 @@ func (r *SafeController) SetSshStatus(ctx http.Context) http.Response {
// GetSshPort 获取 SSH 端口
func (r *SafeController) GetSshPort(ctx http.Context) http.Response {
out, err := tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
out, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
if err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
return Success(ctx, out)
return Success(ctx, cast.ToInt(out))
}
// SetSshPort 设置 SSH 端口
@@ -312,26 +280,25 @@ func (r *SafeController) SetSshPort(ctx http.Context) http.Response {
return Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
oldPort, err := tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
oldPort, err := shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
if err != nil {
return Error(ctx, http.StatusInternalServerError, oldPort)
}
_, _ = tools.Exec("sed -i 's/#Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
_, _ = tools.Exec("sed -i 's/Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
_, _ = shell.Execf("sed -i 's/#Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port)
_, _ = shell.Execf("sed -i 's/Port %s/Port %d/g' /etc/ssh/sshd_config", oldPort, port)
out, err := tools.Exec("systemctl status sshd | grep Active | awk '{print $3}'")
if err != nil || out != "(running)" {
Error(ctx, http.StatusInternalServerError, out)
status, _ := systemctl.Status(r.ssh)
if status {
_ = systemctl.Restart(r.ssh)
}
_, _ = tools.Exec("systemctl restart sshd")
return Success(ctx, nil)
}
// GetPingStatus 获取 Ping 状态
func (r *SafeController) GetPingStatus(ctx http.Context) http.Response {
if tools.IsRHEL() {
out, err := tools.Exec(`firewall-cmd --list-all 2>&1`)
if os.IsRHEL() {
out, err := shell.Execf(`firewall-cmd --list-all`)
if err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
@@ -342,7 +309,7 @@ func (r *SafeController) GetPingStatus(ctx http.Context) http.Response {
return Success(ctx, false)
}
} else {
config, err := tools.Read("/etc/ufw/before.rules")
config, err := io.Read("/etc/ufw/before.rules")
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
@@ -358,17 +325,17 @@ func (r *SafeController) GetPingStatus(ctx http.Context) http.Response {
func (r *SafeController) SetPingStatus(ctx http.Context) http.Response {
var out string
var err error
if tools.IsRHEL() {
if os.IsRHEL() {
if ctx.Request().InputBool("status") {
out, err = tools.Exec(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
out, err = shell.Execf(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
} else {
out, err = tools.Exec(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`)
out, err = shell.Execf(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`)
}
} else {
if ctx.Request().InputBool("status") {
out, err = tools.Exec(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules`)
out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules`)
} else {
out, err = tools.Exec(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules`)
out, err = shell.Execf(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules`)
}
}
@@ -376,10 +343,10 @@ func (r *SafeController) SetPingStatus(ctx http.Context) http.Response {
return Error(ctx, http.StatusInternalServerError, out)
}
if tools.IsRHEL() {
out, err = tools.Exec(`firewall-cmd --reload`)
if os.IsRHEL() {
out, err = shell.Execf(`firewall-cmd --reload`)
} else {
out, err = tools.Exec(`ufw reload`)
out, err = shell.Execf(`ufw reload`)
}
if err != nil {
+146 -60
View File
@@ -3,17 +3,22 @@ package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/path"
"github.com/spf13/cast"
requests "panel/app/http/requests/setting"
responses "panel/app/http/responses/setting"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
requests "github.com/TheTNB/panel/app/http/requests/setting"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/cert"
"github.com/TheTNB/panel/pkg/io"
"github.com/TheTNB/panel/pkg/os"
"github.com/TheTNB/panel/pkg/shell"
"github.com/TheTNB/panel/pkg/tools"
)
type SettingController struct {
setting services.Setting
setting internal.Setting
}
func NewSettingController() *SettingController {
@@ -24,13 +29,12 @@ func NewSettingController() *SettingController {
// List
//
// @Summary 设置列表
// @Description 获取面板设置列表
// @Tags 面板设置
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse{data=responses.Settings}
// @Router /panel/setting/list [get]
// @Summary 设置列表
// @Tags 面板设置
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
// @Router /panel/setting/list [get]
func (r *SettingController) List(ctx http.Context) http.Response {
var settings []models.Setting
err := facades.Orm().Query().Get(&settings)
@@ -41,24 +45,13 @@ func (r *SettingController) List(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
var result responses.Settings
result.Name = r.setting.Get(models.SettingKeyName)
result.Entrance = facades.Config().GetString("http.entrance")
result.WebsitePath = r.setting.Get(models.SettingKeyWebsitePath)
result.BackupPath = r.setting.Get(models.SettingKeyBackupPath)
userID := cast.ToUint(ctx.Value("user_id"))
var user models.User
err = facades.Auth().User(ctx, &user)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
"error": err.Error(),
}).Info("获取用户信息失败")
return ErrorSystem(ctx)
if err = facades.Orm().Query().Where("id", userID).Get(&user); err != nil {
return Error(ctx, http.StatusInternalServerError, "获取用户信息失败")
}
result.Username = user.Username
result.Email = user.Email
result.Port, err = tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
port, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
"error": err.Error(),
@@ -66,23 +59,33 @@ func (r *SettingController) List(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
return Success(ctx, result)
return Success(ctx, http.Json{
"name": r.setting.Get(models.SettingKeyName),
"language": facades.Config().GetString("app.locale"),
"entrance": facades.Config().GetString("http.entrance"),
"ssl": facades.Config().GetBool("panel.ssl"),
"website_path": r.setting.Get(models.SettingKeyWebsitePath),
"backup_path": r.setting.Get(models.SettingKeyBackupPath),
"username": user.Username,
"password": "",
"email": user.Email,
"port": port,
})
}
// Update
//
// @Summary 更新设置
// @Description 更新面板设置
// @Tags 面板设置
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Update true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/setting/update [post]
// @Summary 更新设置
// @Tags 面板设置
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Update true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/setting/update [post]
func (r *SettingController) Update(ctx http.Context) http.Response {
var updateRequest requests.Update
sanitize := Sanitize(ctx, &updateRequest)
sanitize := SanitizeRequest(ctx, &updateRequest)
if sanitize != nil {
return sanitize
}
@@ -95,8 +98,8 @@ func (r *SettingController) Update(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
if !tools.Exists(updateRequest.BackupPath) {
if err = tools.Mkdir(updateRequest.BackupPath, 0644); err != nil {
if !io.Exists(updateRequest.BackupPath) {
if err = io.Mkdir(updateRequest.BackupPath, 0644); err != nil {
return ErrorSystem(ctx)
}
}
@@ -107,11 +110,11 @@ func (r *SettingController) Update(ctx http.Context) http.Response {
}).Info("保存备份目录失败")
return ErrorSystem(ctx)
}
if !tools.Exists(updateRequest.WebsitePath) {
if err = tools.Mkdir(updateRequest.WebsitePath, 0755); err != nil {
if !io.Exists(updateRequest.WebsitePath) {
if err = io.Mkdir(updateRequest.WebsitePath, 0755); err != nil {
return ErrorSystem(ctx)
}
if err = tools.Chown(updateRequest.WebsitePath, "www", "www"); err != nil {
if err = io.Chown(updateRequest.WebsitePath, "www", "www"); err != nil {
return ErrorSystem(ctx)
}
}
@@ -123,11 +126,12 @@ func (r *SettingController) Update(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
userID := cast.ToUint(ctx.Value("user_id"))
var user models.User
err = facades.Auth().User(ctx, &user)
if err != nil {
return ErrorSystem(ctx)
if err = facades.Orm().Query().Where("id", userID).Get(&user); err != nil {
return Error(ctx, http.StatusInternalServerError, "获取用户信息失败")
}
user.Username = updateRequest.UserName
user.Email = updateRequest.Email
if len(updateRequest.Password) > 0 {
@@ -144,7 +148,7 @@ func (r *SettingController) Update(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
oldPort, err := tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
oldPort, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
"error": err.Error(),
@@ -154,50 +158,132 @@ func (r *SettingController) Update(ctx http.Context) http.Response {
port := cast.ToString(updateRequest.Port)
if oldPort != port {
if out, err := tools.Exec("sed -i 's/APP_PORT=" + oldPort + "/APP_PORT=" + port + "/g' /www/panel/panel.conf"); err != nil {
if out, err := shell.Execf("sed -i 's/APP_PORT=%s/APP_PORT=%s/g' /www/panel/panel.conf", oldPort, port); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if tools.IsRHEL() {
if out, err := tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/tcp --permanent 2>&1"); err != nil {
if os.IsRHEL() {
if out, err := shell.Execf("firewall-cmd --remove-port=%s/tcp --permanent", oldPort); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --add-port=" + cast.ToString(port) + "/tcp --permanent 2>&1"); err != nil {
if out, err := shell.Execf("firewall-cmd --add-port=%s/tcp --permanent", port); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("firewall-cmd --reload"); err != nil {
if out, err := shell.Execf("firewall-cmd --reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := tools.Exec("ufw delete allow " + cast.ToString(port) + "/tcp"); err != nil {
if out, err := shell.Execf("ufw delete allow %s/tcp", oldPort); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw allow " + cast.ToString(port) + "/tcp"); err != nil {
if out, err := shell.Execf("ufw allow %s/tcp", port); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
if out, err := tools.Exec("ufw reload"); err != nil {
if out, err := shell.Execf("ufw reload"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
}
oldEntrance, err := tools.Exec(`cat /www/panel/panel.conf | grep APP_ENTRANCE | awk -F '=' '{print $2}' | tr -d '\n'`)
oldEntrance, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_ENTRANCE | awk -F '=' '{print $2}' | tr -d '\n'`)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
"error": err.Error(),
}).Info("获取面板入口失败")
return ErrorSystem(ctx)
}
entrance := cast.ToString(updateRequest.Entrance)
if oldEntrance != entrance {
if out, err := tools.Exec("sed -i 's!APP_ENTRANCE=" + oldEntrance + "!APP_ENTRANCE=" + entrance + "!g' /www/panel/panel.conf"); err != nil {
if out, err := shell.Execf("sed -i 's!APP_ENTRANCE=" + oldEntrance + "!APP_ENTRANCE=" + entrance + "!g' /www/panel/panel.conf"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
if oldPort != port || oldEntrance != entrance {
oldLanguage, err := shell.Execf(`cat /www/panel/panel.conf | grep APP_LOCALE | awk -F '=' '{print $2}' | tr -d '\n'`)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
"error": err.Error(),
}).Info("获取面板语言失败")
return ErrorSystem(ctx)
}
if oldLanguage != updateRequest.Language {
if out, err := shell.Execf("sed -i 's/APP_LOCALE=" + oldLanguage + "/APP_LOCALE=" + updateRequest.Language + "/g' /www/panel/panel.conf"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
if oldPort != port || oldEntrance != entrance || oldLanguage != updateRequest.Language {
tools.RestartPanel()
}
return Success(ctx, nil)
}
// GetHttps
//
// @Summary 获取面板 HTTPS 设置
// @Tags 面板设置
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
// @Router /panel/setting/https [get]
func (r *SettingController) GetHttps(ctx http.Context) http.Response {
certPath := facades.Config().GetString("http.tls.ssl.cert")
keyPath := facades.Config().GetString("http.tls.ssl.key")
crt, err := io.Read(certPath)
if err != nil {
return ErrorSystem(ctx)
}
key, err := io.Read(keyPath)
if err != nil {
return ErrorSystem(ctx)
}
return Success(ctx, http.Json{
"https": facades.Config().GetBool("panel.ssl"),
"cert": crt,
"key": key,
})
}
// UpdateHttps
//
// @Summary 更新面板 HTTPS 设置
// @Tags 面板设置
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Https true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/setting/https [post]
func (r *SettingController) UpdateHttps(ctx http.Context) http.Response {
var httpsRequest requests.Https
sanitize := SanitizeRequest(ctx, &httpsRequest)
if sanitize != nil {
return sanitize
}
if httpsRequest.Https {
if _, err := cert.ParseCert(httpsRequest.Cert); err != nil {
return Error(ctx, http.StatusBadRequest, "证书格式错误")
}
if _, err := cert.ParseKey(httpsRequest.Key); err != nil {
return Error(ctx, http.StatusBadRequest, "密钥格式错误")
}
if err := io.Write(path.Executable("storage/ssl.crt"), httpsRequest.Cert, 0700); err != nil {
return ErrorSystem(ctx)
}
if err := io.Write(path.Executable("storage/ssl.key"), httpsRequest.Key, 0700); err != nil {
return ErrorSystem(ctx)
}
if out, err := shell.Execf("sed -i 's/APP_SSL=false/APP_SSL=true/g' /www/panel/panel.conf"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
} else {
if out, err := shell.Execf("sed -i 's/APP_SSL=true/APP_SSL=false/g' /www/panel/panel.conf"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
tools.RestartPanel()
return Success(ctx, nil)
}
+68 -71
View File
@@ -3,24 +3,24 @@ package controllers
import (
"bytes"
"context"
nethttp "net/http"
"sync"
"time"
"github.com/fasthttp/websocket"
"github.com/goravel/fiber"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/gorilla/websocket"
"github.com/spf13/cast"
"github.com/valyala/fasthttp"
"panel/app/models"
"panel/app/services"
"panel/pkg/ssh"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"github.com/TheTNB/panel/pkg/ssh"
)
type SshController struct {
AuthMethod ssh.AuthMethod
setting services.Setting
setting internal.Setting
}
func NewSshController() *SshController {
@@ -50,33 +50,29 @@ func (r *SshController) GetInfo(ctx http.Context) http.Response {
// UpdateInfo 更新 SSH 配置
func (r *SshController) UpdateInfo(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
if sanitize := Sanitize(ctx, map[string]string{
"host": "required",
"port": "required",
"user": "required",
"password": "required",
})
if err != nil {
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
}
if validator.Fails() {
return Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
}); sanitize != nil {
return sanitize
}
host := ctx.Request().Input("host")
port := ctx.Request().Input("port")
user := ctx.Request().Input("user")
password := ctx.Request().Input("password")
if err = r.setting.Set(models.SettingKeySshHost, host); err != nil {
if err := r.setting.Set(models.SettingKeySshHost, host); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = r.setting.Set(models.SettingKeySshPort, port); err != nil {
if err := r.setting.Set(models.SettingKeySshPort, port); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = r.setting.Set(models.SettingKeySshUser, user); err != nil {
if err := r.setting.Set(models.SettingKeySshUser, user); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = r.setting.Set(models.SettingKeySshPassword, password); err != nil {
if err := r.setting.Set(models.SettingKeySshPassword, password); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
@@ -85,75 +81,76 @@ func (r *SshController) UpdateInfo(ctx http.Context) http.Response {
// Session SSH 会话
func (r *SshController) Session(ctx http.Context) http.Response {
upGrader := websocket.FastHTTPUpgrader{
upGrader := websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
CheckOrigin: func(ctx *fasthttp.RequestCtx) bool {
CheckOrigin: func(r *nethttp.Request) bool {
return true
},
Subprotocols: []string{ctx.Request().Header("Sec-WebSocket-Protocol")},
}
config := ssh.SSHClientConfigPassword(
ws, err := upGrader.Upgrade(ctx.Response().Writer(), ctx.Request().Origin(), nil)
if err != nil {
facades.Log().Tags("面板", "SSH").With(map[string]any{
"error": err.Error(),
}).Infof("建立连接失败")
return ErrorSystem(ctx)
}
defer ws.Close()
config := ssh.ClientConfigPassword(
r.setting.Get(models.SettingKeySshHost)+":"+r.setting.Get(models.SettingKeySshPort),
r.setting.Get(models.SettingKeySshUser),
r.setting.Get(models.SettingKeySshPassword),
)
client, err := ssh.NewSSHClient(config)
err = upGrader.Upgrade(ctx.(*fiber.Context).Instance().Context(), func(conn *websocket.Conn) {
if err != nil {
_ = conn.WriteControl(websocket.CloseMessage,
[]byte(err.Error()), time.Now().Add(time.Second))
return
}
defer client.Close()
turn, err := ssh.NewTurn(conn, client)
if err != nil {
_ = conn.WriteControl(websocket.CloseMessage,
[]byte(err.Error()), time.Now().Add(time.Second))
return
}
defer turn.Close()
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
var logBuff = bufPool.Get().(*bytes.Buffer)
logBuff.Reset()
defer bufPool.Put(logBuff)
ctx2, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
err := turn.LoopRead(logBuff, ctx2)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "SSH").With(map[string]any{
"error": err.Error(),
}).Info("SSH 会话错误")
}
}()
go func() {
defer wg.Done()
err := turn.SessionWait()
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "SSH").With(map[string]any{
"error": err.Error(),
}).Info("SSH 会话错误")
}
cancel()
}()
wg.Wait()
})
if err != nil {
_ = ws.WriteControl(websocket.CloseMessage,
[]byte(err.Error()), time.Now().Add(time.Second))
return ErrorSystem(ctx)
}
defer client.Close()
turn, err := ssh.NewTurn(ws, client)
if err != nil {
_ = ws.WriteControl(websocket.CloseMessage,
[]byte(err.Error()), time.Now().Add(time.Second))
return ErrorSystem(ctx)
}
defer turn.Close()
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
var logBuff = bufPool.Get().(*bytes.Buffer)
logBuff.Reset()
defer bufPool.Put(logBuff)
sshCtx, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
if err = turn.LoopRead(logBuff, sshCtx); err != nil {
facades.Log().Tags("面板", "SSH").With(map[string]any{
"error": err.Error(),
}).Infof("读取数据失败")
}
}()
go func() {
defer wg.Done()
if err = turn.SessionWait(); err != nil {
facades.Log().Tags("面板", "SSH").With(map[string]any{
"error": err.Error(),
}).Infof("会话错误")
}
cancel()
}()
wg.Wait()
return nil
}
+7 -165
View File
@@ -1,35 +1,18 @@
package controllers
import (
"html/template"
"io"
"path/filepath"
"regexp"
"strings"
"github.com/goravel/framework/contracts/http"
swaggerFiles "github.com/swaggo/files/v2"
"github.com/swaggo/swag"
"github.com/swaggo/http-swagger/v2"
_ "github.com/TheTNB/panel/docs"
)
type SwaggerController struct {
// Dependent services
}
// Config stores fiberSwagger configuration variables.
type Config struct {
URL string
InstanceName string
DocExpansion string
DomID string
DeepLinking bool
PersistAuthorization bool
}
func NewSwaggerController() *SwaggerController {
return &SwaggerController{
// Inject services
}
return &SwaggerController{}
}
// Index
@@ -41,149 +24,8 @@ func NewSwaggerController() *SwaggerController {
// @Failure 500
// @Router /swagger [get]
func (r *SwaggerController) Index(ctx http.Context) http.Response {
config := Config{
URL: "swagger.json",
DocExpansion: "list",
DomID: "swagger-ui",
InstanceName: swag.Name,
DeepLinking: true,
}
handler := httpSwagger.Handler()
handler(ctx.Response().Writer(), ctx.Request().Origin())
// create a template with name
index, _ := template.New("swagger_index.html").Parse(indexTemplate)
var re = regexp.MustCompile(`^(.*/)([^?].*)?[?|.]*$`)
matches := re.FindStringSubmatch(ctx.Request().Path())
path := matches[2]
fileExt := filepath.Ext(path)
switch path {
case "":
return ctx.Response().Redirect(http.StatusMovedPermanently, filepath.Join(matches[1], "index.html"))
case "index.html":
var builder strings.Builder
err := index.Execute(&builder, config)
if err != nil {
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
}
return ctx.Response().Success().String("html", builder.String())
case "swagger.json":
return ctx.Response().File("docs/swagger.json")
}
file, err := swaggerFiles.FS.Open(path)
if err != nil {
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
}
return ctx.Response().Success().String(fileExt[0:], string(content))
return nil
}
const indexTemplate = `<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link href="https://gfont.cdn.haozi.net/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
<defs>
<symbol viewBox="0 0 20 20" id="unlocked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
</symbol>
<symbol viewBox="0 0 20 20" id="locked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="close">
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow">
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
</symbol>
<symbol viewBox="0 0 20 20" id="large-arrow-down">
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="jump-to">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
</symbol>
<symbol viewBox="0 0 24 24" id="expand">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
</symbol>
</defs>
</svg>
<div id="swagger-ui"></div>
<script src="./swagger-ui-bundle.js"> </script>
<script src="./swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Build a system
const ui = SwaggerUIBundle({
url: "{{.URL}}",
deepLinking: {{.DeepLinking}},
docExpansion: "{{.DocExpansion}}",
dom_id: "#{{.DomID}}",
persistAuthorization: {{.PersistAuthorization}},
validatorUrl: null,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
window.ui = ui
}
</script>
</body>
</html>
`
+210
View File
@@ -0,0 +1,210 @@
package controllers
import (
"fmt"
"github.com/goravel/framework/contracts/http"
"github.com/TheTNB/panel/pkg/systemctl"
)
type SystemController struct {
}
func NewSystemController() *SystemController {
return &SystemController{}
}
// ServiceStatus
//
// @Summary 服务状态
// @Tags 系统
// @Produce json
// @Security BearerToken
// @Param data query string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/system/service/status [get]
func (r *SystemController) ServiceStatus(ctx http.Context) http.Response {
if sanitize := Sanitize(ctx, map[string]string{
"service": "required|string",
}); sanitize != nil {
return sanitize
}
service := ctx.Request().Query("service")
status, err := systemctl.Status(service)
if err != nil {
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务运行状态失败", service))
}
return Success(ctx, status)
}
// ServiceIsEnabled
//
// @Summary 是否启用服务
// @Tags 系统
// @Produce json
// @Security BearerToken
// @Param data query string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/system/service/isEnabled [get]
func (r *SystemController) ServiceIsEnabled(ctx http.Context) http.Response {
if sanitize := Sanitize(ctx, map[string]string{
"service": "required|string",
}); sanitize != nil {
return sanitize
}
service := ctx.Request().Query("service")
enabled, err := systemctl.IsEnabled(service)
if err != nil {
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("获取 %s 服务启用状态失败", service))
}
return Success(ctx, enabled)
}
// ServiceEnable
//
// @Summary 启用服务
// @Tags 系统
// @Produce json
// @Security BearerToken
// @Param data body string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/system/service/enable [post]
func (r *SystemController) ServiceEnable(ctx http.Context) http.Response {
if sanitize := Sanitize(ctx, map[string]string{
"service": "required|string",
}); sanitize != nil {
return sanitize
}
service := ctx.Request().Input("service")
if err := systemctl.Enable(service); err != nil {
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("启用 %s 服务失败", service))
}
return Success(ctx, nil)
}
// ServiceDisable
//
// @Summary 禁用服务
// @Tags 系统
// @Produce json
// @Security BearerToken
// @Param data body string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/system/service/disable [post]
func (r *SystemController) ServiceDisable(ctx http.Context) http.Response {
if sanitize := Sanitize(ctx, map[string]string{
"service": "required|string",
}); sanitize != nil {
return sanitize
}
service := ctx.Request().Input("service")
if err := systemctl.Disable(service); err != nil {
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("禁用 %s 服务失败", service))
}
return Success(ctx, nil)
}
// ServiceRestart
//
// @Summary 重启服务
// @Tags 系统
// @Produce json
// @Security BearerToken
// @Param data body string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/system/service/restart [post]
func (r *SystemController) ServiceRestart(ctx http.Context) http.Response {
if sanitize := Sanitize(ctx, map[string]string{
"service": "required|string",
}); sanitize != nil {
return sanitize
}
service := ctx.Request().Input("service")
if err := systemctl.Restart(service); err != nil {
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重启 %s 服务失败", service))
}
return Success(ctx, nil)
}
// ServiceReload
//
// @Summary 重载服务
// @Tags 系统
// @Produce json
// @Security BearerToken
// @Param data body string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/system/service/reload [post]
func (r *SystemController) ServiceReload(ctx http.Context) http.Response {
if sanitize := Sanitize(ctx, map[string]string{
"service": "required|string",
}); sanitize != nil {
return sanitize
}
service := ctx.Request().Input("service")
if err := systemctl.Reload(service); err != nil {
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载 %s 服务失败", service))
}
return Success(ctx, nil)
}
// ServiceStart
//
// @Summary 启动服务
// @Tags 系统
// @Produce json
// @Security BearerToken
// @Param data body string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/system/service/start [post]
func (r *SystemController) ServiceStart(ctx http.Context) http.Response {
if sanitize := Sanitize(ctx, map[string]string{
"service": "required|string",
}); sanitize != nil {
return sanitize
}
service := ctx.Request().Input("service")
if err := systemctl.Start(service); err != nil {
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("启动 %s 服务失败", service))
}
return Success(ctx, nil)
}
// ServiceStop
//
// @Summary 停止服务
// @Tags 系统
// @Produce json
// @Security BearerToken
// @Param data body string true "request"
// @Success 200 {object} controllers.SuccessResponse
// @Router /panel/system/service/stop [post]
func (r *SystemController) ServiceStop(ctx http.Context) http.Response {
if sanitize := Sanitize(ctx, map[string]string{
"service": "required|string",
}); sanitize != nil {
return sanitize
}
service := ctx.Request().Input("service")
if err := systemctl.Stop(service); err != nil {
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("停止 %s 服务失败", service))
}
return Success(ctx, nil)
}
+5 -4
View File
@@ -3,8 +3,9 @@ package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/pkg/tools"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/pkg/shell"
)
type TaskController struct {
@@ -62,9 +63,9 @@ func (r *TaskController) Log(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
log, err := tools.Exec("tail -n 1000 " + task.Log)
log, err := shell.Execf(`tail -n 500 '` + task.Log + `'`)
if err != nil {
return Error(ctx, http.StatusInternalServerError, log)
return Error(ctx, http.StatusInternalServerError, "日志已被清理")
}
return Success(ctx, log)
+40 -38
View File
@@ -3,10 +3,10 @@ package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
"panel/app/http/requests/user"
responses "panel/app/http/responses/user"
"panel/app/models"
"github.com/TheTNB/panel/app/http/requests/user"
"github.com/TheTNB/panel/app/models"
)
type UserController struct {
@@ -21,19 +21,18 @@ func NewUserController() *UserController {
// Login
//
// @Summary 登录
// @Description 通过用户名和密码获取访问令牌
// @Tags 用户鉴权
// @Accept json
// @Produce json
// @Param data body requests.Login true "request"
// @Success 200 {object} SuccessResponse
// @Failure 403 {object} ErrorResponse "用户名或密码错误"
// @Failure 500 {object} ErrorResponse "系统内部错误
// @Router /panel/user/login [post]
// @Summary 登录
// @Tags 用户鉴权
// @Accept json
// @Produce json
// @Param data body requests.Login true "request"
// @Success 200 {object} SuccessResponse
// @Failure 403 {object} ErrorResponse "用户名或密码错误"
// @Failure 500 {object} ErrorResponse "系统内部错误
// @Router /panel/user/login [post]
func (r *UserController) Login(ctx http.Context) http.Response {
var loginRequest requests.Login
sanitize := Sanitize(ctx, &loginRequest)
sanitize := SanitizeRequest(ctx, &loginRequest)
if sanitize != nil {
return sanitize
}
@@ -61,42 +60,45 @@ func (r *UserController) Login(ctx http.Context) http.Response {
}
}
token, loginErr := facades.Auth().LoginUsingID(ctx, user.ID)
if loginErr != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
"error": err.Error(),
}).Info("登录失败")
return ErrorSystem(ctx)
}
ctx.Request().Session().Put("user_id", user.ID)
return Success(ctx, nil)
}
return Success(ctx, http.Json{
"access_token": token,
})
// Logout
//
// @Summary 登出
// @Tags 用户鉴权
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
// @Router /panel/user/logout [post]
func (r *UserController) Logout(ctx http.Context) http.Response {
ctx.Request().Session().Forget("user_id")
return Success(ctx, nil)
}
// Info
//
// @Summary 用户信息
// @Description 获取当前登录用户信息
// @Tags 用户鉴权
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse{data=responses.Info}
// @Router /panel/user/info [get]
// @Summary 用户信息
// @Tags 用户鉴权
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse
// @Router /panel/user/info [get]
func (r *UserController) Info(ctx http.Context) http.Response {
userID := cast.ToUint(ctx.Value("user_id"))
var user models.User
err := facades.Auth().User(ctx, &user)
if err != nil {
if err := facades.Orm().Query().Where("id", userID).Get(&user); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
"error": err.Error(),
}).Info("获取用户信息失败")
return ErrorSystem(ctx)
}
return Success(ctx, responses.Info{
ID: user.ID,
Role: []string{"admin"},
Username: user.Username,
Email: user.Email,
return Success(ctx, http.Json{
"id": user.ID,
"role": []string{"admin"},
"username": user.Username,
"email": user.Email,
})
}
+194 -213
View File
@@ -8,18 +8,22 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
commonrequests "panel/app/http/requests/common"
requests "panel/app/http/requests/website"
responses "panel/app/http/responses/website"
"panel/app/models"
"panel/app/services"
"panel/pkg/tools"
commonrequests "github.com/TheTNB/panel/app/http/requests/common"
requests "github.com/TheTNB/panel/app/http/requests/website"
"github.com/TheTNB/panel/app/models"
"github.com/TheTNB/panel/internal"
"github.com/TheTNB/panel/internal/services"
"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/types"
)
type WebsiteController struct {
website services.Website
setting services.Setting
backup services.Backup
website internal.Website
setting internal.Setting
backup internal.Backup
}
func NewWebsiteController() *WebsiteController {
@@ -32,17 +36,16 @@ func NewWebsiteController() *WebsiteController {
// List
//
// @Summary 获取网站列表
// @Description 获取网站管理的网站列表
// @Tags 网站管理
// @Produce json
// @Security BearerToken
// @Param data body commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse{data=responses.List}
// @Router /panel/websites [get]
// @Summary 获取网站列表
// @Tags 网站
// @Produce json
// @Security BearerToken
// @Param data query commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites [get]
func (r *WebsiteController) List(ctx http.Context) http.Response {
var paginateRequest commonrequests.Paginate
sanitize := Sanitize(ctx, &paginateRequest)
sanitize := SanitizeRequest(ctx, &paginateRequest)
if sanitize != nil {
return sanitize
}
@@ -55,26 +58,25 @@ func (r *WebsiteController) List(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
return Success(ctx, responses.List{
Total: total,
Items: websites,
return Success(ctx, http.Json{
"total": total,
"items": websites,
})
}
// Add
//
// @Summary 添加网站
// @Description 添加网站到网站管理
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Add true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites [post]
// @Summary 添加网站
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Add true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites [post]
func (r *WebsiteController) Add(ctx http.Context) http.Response {
var addRequest requests.Add
sanitize := Sanitize(ctx, &addRequest)
sanitize := SanitizeRequest(ctx, &addRequest)
if sanitize != nil {
return sanitize
}
@@ -83,7 +85,7 @@ func (r *WebsiteController) Add(ctx http.Context) http.Response {
addRequest.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + addRequest.Name
}
website := services.PanelWebsite{
website := types.WebsiteAdd{
Name: addRequest.Name,
Status: true,
Domains: addRequest.Domains,
@@ -111,29 +113,27 @@ func (r *WebsiteController) Add(ctx http.Context) http.Response {
// Delete
//
// @Summary 删除网站
// @Description 删除网站管理的网站
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id} [delete]
// @Summary 删除网站
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Delete true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/delete [post]
func (r *WebsiteController) Delete(ctx http.Context) http.Response {
var idRequest requests.ID
sanitize := Sanitize(ctx, &idRequest)
var deleteRequest requests.Delete
sanitize := SanitizeRequest(ctx, &deleteRequest)
if sanitize != nil {
return sanitize
}
err := r.website.Delete(idRequest.ID)
if err != nil {
if err := r.website.Delete(deleteRequest); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"id": idRequest.ID,
"id": deleteRequest.ID,
"error": err.Error(),
}).Info("删除网站失败")
return Error(ctx, http.StatusInternalServerError, "删除网站失败: "+err.Error())
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
@@ -141,19 +141,18 @@ func (r *WebsiteController) Delete(ctx http.Context) http.Response {
// GetDefaultConfig
//
// @Summary 获取默认配置
// @Description 获取默认首页和停止页配置
// @Tags 网站管理
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse{data=map[string]string}
// @Router /panel/website/defaultConfig [get]
// @Summary 获取默认配置
// @Tags 网站
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse{data=map[string]string}
// @Router /panel/website/defaultConfig [get]
func (r *WebsiteController) GetDefaultConfig(ctx http.Context) http.Response {
index, err := tools.Read("/www/server/openresty/html/index.html")
index, err := io.Read("/www/server/openresty/html/index.html")
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
stop, err := tools.Read("/www/server/openresty/html/stop.html")
stop, err := io.Read("/www/server/openresty/html/stop.html")
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
@@ -166,27 +165,26 @@ func (r *WebsiteController) GetDefaultConfig(ctx http.Context) http.Response {
// SaveDefaultConfig
//
// @Summary 保存默认配置
// @Description 保存默认首页和停止页配置
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body map[string]string true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/website/defaultConfig [post]
// @Summary 保存默认配置
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body map[string]string true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/website/defaultConfig [post]
func (r *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response {
index := ctx.Request().Input("index")
stop := ctx.Request().Input("stop")
if err := tools.Write("/www/server/openresty/html/index.html", index, 0644); err != nil {
if err := io.Write("/www/server/openresty/html/index.html", index, 0644); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"error": err.Error(),
}).Info("保存默认首页配置失败")
return ErrorSystem(ctx)
}
if err := tools.Write("/www/server/openresty/html/stop.html", stop, 0644); err != nil {
if err := io.Write("/www/server/openresty/html/stop.html", stop, 0644); err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"error": err.Error(),
}).Info("保存默认停止页配置失败")
@@ -198,18 +196,17 @@ func (r *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response {
// GetConfig
//
// @Summary 获取配置
// @Description 获取网站的配置
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse{data=services.PanelWebsite}
// @Router /panel/websites/{id}/config [get]
// @Summary 获取网站配置
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse{data=types.WebsiteAdd}
// @Router /panel/websites/{id}/config [get]
func (r *WebsiteController) GetConfig(ctx http.Context) http.Response {
var idRequest requests.ID
sanitize := Sanitize(ctx, &idRequest)
sanitize := SanitizeRequest(ctx, &idRequest)
if sanitize != nil {
return sanitize
}
@@ -228,19 +225,18 @@ func (r *WebsiteController) GetConfig(ctx http.Context) http.Response {
// SaveConfig
//
// @Summary 保存配置
// @Description 保存网站的配置
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Param data body requests.SaveConfig true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/config [post]
// @Summary 保存网站配置
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Param data body requests.SaveConfig true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/config [post]
func (r *WebsiteController) SaveConfig(ctx http.Context) http.Response {
var saveConfigRequest requests.SaveConfig
sanitize := Sanitize(ctx, &saveConfigRequest)
sanitize := SanitizeRequest(ctx, &saveConfigRequest)
if sanitize != nil {
return sanitize
}
@@ -255,18 +251,17 @@ func (r *WebsiteController) SaveConfig(ctx http.Context) http.Response {
// ClearLog
//
// @Summary 清空日志
// @Description 清空网站的日志
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/log [delete]
// @Summary 清空网站日志
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/log [delete]
func (r *WebsiteController) ClearLog(ctx http.Context) http.Response {
var idRequest requests.ID
sanitize := Sanitize(ctx, &idRequest)
sanitize := SanitizeRequest(ctx, &idRequest)
if sanitize != nil {
return sanitize
}
@@ -277,7 +272,7 @@ func (r *WebsiteController) ClearLog(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
if err := tools.Remove("/www/wwwlogs/" + website.Name + ".log"); err != nil {
if err := io.Remove("/www/wwwlogs/" + website.Name + ".log"); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
@@ -286,18 +281,17 @@ func (r *WebsiteController) ClearLog(ctx http.Context) http.Response {
// UpdateRemark
//
// @Summary 更新备注
// @Description 更新网站的备注
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/updateRemark [post]
// @Summary 更新网站备注
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/updateRemark [post]
func (r *WebsiteController) UpdateRemark(ctx http.Context) http.Response {
var idRequest requests.ID
sanitize := Sanitize(ctx, &idRequest)
sanitize := SanitizeRequest(ctx, &idRequest)
if sanitize != nil {
return sanitize
}
@@ -322,22 +316,21 @@ func (r *WebsiteController) UpdateRemark(ctx http.Context) http.Response {
// BackupList
//
// @Summary 获取备份列表
// @Description 获取网站的备份列表
// @Tags 网站管理
// @Produce json
// @Security BearerToken
// @Param data body commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse{data=[]services.BackupFile}
// @Router /panel/website/backupList [get]
// @Summary 获取网站备份列表
// @Tags 网站
// @Produce json
// @Security BearerToken
// @Param data query commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse{data=[]types.BackupFile}
// @Router /panel/website/backupList [get]
func (r *WebsiteController) BackupList(ctx http.Context) http.Response {
var paginateRequest commonrequests.Paginate
sanitize := Sanitize(ctx, &paginateRequest)
sanitize := SanitizeRequest(ctx, &paginateRequest)
if sanitize != nil {
return sanitize
}
backupList, err := r.backup.WebsiteList()
backups, err := r.backup.WebsiteList()
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "网站管理").With(map[string]any{
"error": err.Error(),
@@ -345,42 +338,27 @@ func (r *WebsiteController) BackupList(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
startIndex := (paginateRequest.Page - 1) * paginateRequest.Limit
endIndex := paginateRequest.Page * paginateRequest.Limit
if startIndex > len(backupList) {
return Success(ctx, http.Json{
"total": 0,
"items": []services.BackupFile{},
})
}
if endIndex > len(backupList) {
endIndex = len(backupList)
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []services.BackupFile{}
}
paged, total := Paginate(ctx, backups)
return Success(ctx, http.Json{
"total": len(backupList),
"items": pagedBackupList,
"total": total,
"items": paged,
})
}
// CreateBackup
//
// @Summary 创建备份
// @Description 创建网站的备份
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/createBackup [post]
// @Summary 创建网站备份
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/createBackup [post]
func (r *WebsiteController) CreateBackup(ctx http.Context) http.Response {
var idRequest requests.ID
sanitize := Sanitize(ctx, &idRequest)
sanitize := SanitizeRequest(ctx, &idRequest)
if sanitize != nil {
return sanitize
}
@@ -407,15 +385,14 @@ func (r *WebsiteController) CreateBackup(ctx http.Context) http.Response {
// UploadBackup
//
// @Summary 上传备份
// @Description 上传网站的备份
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param file formData file true "备份文件"
// @Success 200 {object} SuccessResponse
// @Router /panel/website/uploadBackup [put]
// @Summary 上传网站备份
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param file formData file true "备份文件"
// @Success 200 {object} SuccessResponse
// @Router /panel/website/uploadBackup [put]
func (r *WebsiteController) UploadBackup(ctx http.Context) http.Response {
file, err := ctx.Request().File("file")
if err != nil {
@@ -423,8 +400,8 @@ func (r *WebsiteController) UploadBackup(ctx http.Context) http.Response {
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/website"
if !tools.Exists(backupPath) {
if err = tools.Mkdir(backupPath, 0644); err != nil {
if !io.Exists(backupPath) {
if err = io.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
@@ -443,18 +420,17 @@ func (r *WebsiteController) UploadBackup(ctx http.Context) http.Response {
// RestoreBackup
//
// @Summary 还原备份
// @Description 还原网站的备份
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/restoreBackup [post]
// @Summary 还原网站备份
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/restoreBackup [post]
func (r *WebsiteController) RestoreBackup(ctx http.Context) http.Response {
var restoreBackupRequest requests.RestoreBackup
sanitize := Sanitize(ctx, &restoreBackupRequest)
sanitize := SanitizeRequest(ctx, &restoreBackupRequest)
if sanitize != nil {
return sanitize
}
@@ -478,30 +454,29 @@ func (r *WebsiteController) RestoreBackup(ctx http.Context) http.Response {
// DeleteBackup
//
// @Summary 删除备份
// @Description 删除网站的备份
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.DeleteBackup true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/website/deleteBackup [delete]
// @Summary 删除网站备份
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.DeleteBackup true "request"
// @Success 200 {object} SuccessResponse
// @Router /panel/website/deleteBackup [delete]
func (r *WebsiteController) DeleteBackup(ctx http.Context) http.Response {
var deleteBackupRequest requests.DeleteBackup
sanitize := Sanitize(ctx, &deleteBackupRequest)
sanitize := SanitizeRequest(ctx, &deleteBackupRequest)
if sanitize != nil {
return sanitize
}
backupPath := r.setting.Get(models.SettingKeyBackupPath) + "/website"
if !tools.Exists(backupPath) {
if err := tools.Mkdir(backupPath, 0644); err != nil {
if !io.Exists(backupPath) {
if err := io.Mkdir(backupPath, 0644); err != nil {
return nil
}
}
if err := tools.Remove(backupPath + "/" + deleteBackupRequest.Name); err != nil {
if err := io.Remove(backupPath + "/" + deleteBackupRequest.Name); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
@@ -510,18 +485,17 @@ func (r *WebsiteController) DeleteBackup(ctx http.Context) http.Response {
// ResetConfig
//
// @Summary 重置配置
// @Description 重置网站的配置
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/resetConfig [post]
// @Summary 重置网站配置
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/resetConfig [post]
func (r *WebsiteController) ResetConfig(ctx http.Context) http.Response {
var idRequest requests.ID
sanitize := Sanitize(ctx, &idRequest)
sanitize := SanitizeRequest(ctx, &idRequest)
if sanitize != nil {
return sanitize
}
@@ -567,7 +541,7 @@ server
# php标记位结束
# waf标记位开始
waf on;
waf off;
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
waf_mode DYNAMIC;
waf_cc_deny rate=1000r/m duration=60m;
@@ -575,9 +549,12 @@ server
# waf标记位结束
# 错误页配置,可自行设置
#error_page 404 /404.html;
error_page 404 /404.html;
#error_page 502 /502.html;
# acme证书签发配置,不可修改
include /www/server/vhost/acme/%s.conf;
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
include /www/server/vhost/rewrite/%s.conf;
@@ -598,15 +575,19 @@ server
error_log /www/wwwlogs/%s.log;
}
`, website.Path, website.Php, website.Name, website.Name, website.Name)
if err := tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil {
`, website.Path, website.Php, website.Name, website.Name, website.Name, website.Name)
if err := io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil {
return nil
}
if err := tools.Write("/www/server/vhost/rewrite"+website.Name+".conf", "", 0644); err != nil {
if err := io.Write("/www/server/vhost/rewrite/"+website.Name+".conf", "", 0644); err != nil {
return nil
}
if exec, err := tools.Exec("systemctl reload openresty"); err != nil {
return Error(ctx, http.StatusInternalServerError, exec)
if err := io.Write("/www/server/vhost/acme/"+website.Name+".conf", "", 0644); err != nil {
return nil
}
if err := systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return Success(ctx, nil)
@@ -614,18 +595,17 @@ server
// Status
//
// @Summary 状态
// @Description 启用或停用网站
// @Tags 网站管理
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/status [post]
// @Summary 获取网站状态
// @Tags 网站
// @Accept json
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse
// @Router /panel/websites/{id}/status [post]
func (r *WebsiteController) Status(ctx http.Context) http.Response {
var idRequest requests.ID
sanitize := Sanitize(ctx, &idRequest)
sanitize := SanitizeRequest(ctx, &idRequest)
if sanitize != nil {
return sanitize
}
@@ -640,13 +620,13 @@ func (r *WebsiteController) Status(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
raw, err := tools.Read("/www/server/vhost/" + website.Name + ".conf")
raw, err := io.Read("/www/server/vhost/" + website.Name + ".conf")
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
// 运行目录
rootConfig := tools.Cut(raw, "# root标记位开始\n", "# root标记位结束")
rootConfig := str.Cut(raw, "# root标记位开始\n", "# root标记位结束")
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig)
if len(match) == 2 {
if website.Status {
@@ -658,7 +638,7 @@ func (r *WebsiteController) Status(ctx http.Context) http.Response {
}
// 默认文件
indexConfig := tools.Cut(raw, "# index标记位开始\n", "# index标记位结束")
indexConfig := str.Cut(raw, "# index标记位开始\n", "# index标记位结束")
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig)
if len(match) == 2 {
if website.Status {
@@ -669,11 +649,12 @@ func (r *WebsiteController) Status(ctx http.Context) http.Response {
}
}
if err := tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil {
if err = io.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644); err != nil {
return ErrorSystem(ctx)
}
if exec, err := tools.Exec("systemctl reload openresty"); err != nil {
return Error(ctx, http.StatusInternalServerError, exec)
if err = systemctl.Reload("openresty"); err != nil {
_, err = shell.Execf("openresty -t")
return Error(ctx, http.StatusInternalServerError, fmt.Sprintf("重载OpenResty失败: %v", err))
}
return Success(ctx, nil)
+10 -1
View File
@@ -2,6 +2,9 @@ package http
import (
"github.com/goravel/framework/contracts/http"
sessionmiddleware "github.com/goravel/framework/session/middleware"
"github.com/TheTNB/panel/app/http/middleware"
)
type Kernel struct {
@@ -10,5 +13,11 @@ type Kernel struct {
// The application's global HTTP middleware stack.
// These middleware are run during every request to your application.
func (kernel Kernel) Middleware() []http.Middleware {
return []http.Middleware{}
return []http.Middleware{
sessionmiddleware.StartSession(),
middleware.Log(),
middleware.Status(),
middleware.Entrance(),
middleware.Static(),
}
}
+38
View File
@@ -0,0 +1,38 @@
package middleware
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
)
func Entrance() http.Middleware {
return func(ctx http.Context) {
translate := facades.Lang(ctx)
if !ctx.Request().HasSession() {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.session.missing"),
})
return
}
entrance := facades.Config().GetString("http.entrance")
if ctx.Request().Path() == entrance {
ctx.Request().Session().Put("verify_entrance", true)
ctx.Response().Redirect(http.StatusFound, "/")
return
}
if !facades.Config().GetBool("app.debug") &&
(ctx.Request().Session().Missing("verify_entrance") || !cast.ToBool(ctx.Request().Session().Get("verify_entrance"))) &&
ctx.Request().Path() != "/robots.txt" {
ctx.Request().AbortWithStatusJson(http.StatusTeapot, http.Json{
"message": "请通过正确的入口访问",
})
return
}
ctx.Request().Next()
}
}
-49
View File
@@ -1,49 +0,0 @@
package middleware
import (
"errors"
"github.com/goravel/framework/auth"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
)
// Jwt 确保通过 JWT 鉴权
func Jwt() http.Middleware {
return func(ctx http.Context) {
token := ctx.Request().Header("Authorization", ctx.Request().Header("Sec-WebSocket-Protocol"))
if len(token) == 0 {
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": 401,
"message": "未登录",
})
return
}
// JWT 鉴权
if _, err := facades.Auth().Parse(ctx, token); err != nil {
if errors.Is(err, auth.ErrorTokenExpired) {
token, err = facades.Auth().Refresh(ctx)
if err != nil {
// 到达刷新时间上限
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": 401,
"message": "登录已过期",
})
return
}
token = "Bearer " + token
} else {
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": 401,
"message": "登录已过期",
})
return
}
}
ctx.Response().Header("Authorization", token)
ctx.Request().Next()
}
}
+20
View File
@@ -0,0 +1,20 @@
package middleware
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
)
// Log 记录请求日志
func Log() http.Middleware {
return func(ctx http.Context) {
facades.Log().Channel("http").With(map[string]any{
"Method": ctx.Request().Method(),
"URL": ctx.Request().FullUrl(),
"IP": ctx.Request().Ip(),
"UA": ctx.Request().Header("User-Agent"),
"Body": ctx.Request().All(),
}).Info("HTTP Request")
ctx.Request().Next()
}
}
+31 -25
View File
@@ -2,26 +2,29 @@ package middleware
import (
"strings"
"sync"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/translation"
"github.com/goravel/framework/facades"
"panel/app/services"
"github.com/TheTNB/panel/internal/services"
)
// MustInstall 确保已安装插件
func MustInstall() http.Middleware {
return func(ctx http.Context) {
path := ctx.Request().Path()
translate := facades.Lang(ctx)
var slug string
if strings.HasPrefix(path, "/api/panel/website") {
slug = "openresty"
} else if strings.HasPrefix(path, "/api/panel/container") {
slug = "podman"
} else {
pathArr := strings.Split(path, "/")
if len(pathArr) < 4 {
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": http.StatusForbidden,
"message": "插件不存在",
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
"message": translate.Get("errors.plugin.notExist"),
})
return
}
@@ -32,51 +35,54 @@ func MustInstall() http.Middleware {
installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug)
installedPlugins, err := services.NewPluginImpl().AllInstalled()
if err != nil {
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": http.StatusInternalServerError,
"message": "系统内部错误",
ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, http.Json{
"message": translate.Get("errors.internal"),
})
return
}
if installedPlugin.Version != plugin.Version || installedPlugin.Slug != plugin.Slug {
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": http.StatusForbidden,
"message": "插件 " + slug + " 需要更新至 " + plugin.Version + " 版本",
if installedPlugin.Slug != plugin.Slug {
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
"message": translate.Get("errors.plugin.notInstalled", translation.Option{
Replace: map[string]string{
"slug": slug,
},
}),
})
return
}
var lock sync.RWMutex
pluginsMap := make(map[string]bool)
for _, p := range installedPlugins {
lock.Lock()
pluginsMap[p.Slug] = true
lock.Unlock()
}
for _, require := range plugin.Requires {
lock.RLock()
_, requireFound := pluginsMap[require]
lock.RUnlock()
if !requireFound {
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": http.StatusForbidden,
"message": "插件 " + slug + " 需要依赖 " + require + " 插件",
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
"message": translate.Get("errors.plugin.dependent", translation.Option{
Replace: map[string]string{
"slug": slug,
"dependency": require,
},
}),
})
return
}
}
for _, exclude := range plugin.Excludes {
lock.RLock()
_, excludeFound := pluginsMap[exclude]
lock.RUnlock()
if excludeFound {
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": http.StatusForbidden,
"message": "插件 " + slug + " 不兼容 " + exclude + " 插件",
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
"message": translate.Get("errors.plugin.incompatible", translation.Option{
Replace: map[string]string{
"slug": slug,
"exclude": exclude,
},
}),
})
return
}
+53
View File
@@ -0,0 +1,53 @@
package middleware
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/spf13/cast"
)
// Session 确保通过 JWT 鉴权
func Session() http.Middleware {
return func(ctx http.Context) {
translate := facades.Lang(ctx)
if !ctx.Request().HasSession() {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.session.missing"),
})
return
}
if ctx.Request().Session().Missing("user_id") {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.session.expired"),
})
return
}
userID := cast.ToUint(ctx.Request().Session().Get("user_id"))
if userID == 0 {
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.session.invalid"),
})
return
}
// 刷新会话
/*if err := ctx.Request().Session().Regenerate(); err == nil {
ctx.Response().Cookie(http.Cookie{
Name: ctx.Request().Session().GetName(),
Value: ctx.Request().Session().GetID(),
MaxAge: facades.Config().GetInt("session.lifetime") * 60,
Path: facades.Config().GetString("session.path"),
Domain: facades.Config().GetString("session.domain"),
Secure: facades.Config().GetBool("session.secure"),
HttpOnly: facades.Config().GetBool("session.http_only"),
SameSite: facades.Config().GetString("session.same_site"),
})
}*/
ctx.WithValue("user_id", userID)
ctx.Request().Next()
}
}
+16
View File
@@ -0,0 +1,16 @@
package middleware
import (
"github.com/gin-contrib/static"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/gin"
"github.com/TheTNB/panel/embed"
)
func Static() http.Middleware {
return func(ctx http.Context) {
static.Serve("/", static.EmbedFolder(embed.PublicFS, "frontend"))(ctx.(*gin.Context).Instance())
ctx.Request().Next()
}
}
+40
View File
@@ -0,0 +1,40 @@
package middleware
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/TheTNB/panel/pkg/types"
)
// Status 检查程序状态
func Status() http.Middleware {
return func(ctx http.Context) {
translate := facades.Lang(ctx)
switch types.Status {
case types.StatusUpgrade:
ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{
"message": translate.Get("status.upgrade"),
})
return
case types.StatusMaintain:
ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{
"message": translate.Get("status.maintain"),
})
return
case types.StatusClosed:
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
"message": translate.Get("status.closed"),
})
return
case types.StatusFailed:
ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, http.Json{
"message": translate.Get("status.failed"),
})
return
default:
ctx.Request().Next()
return
}
}
}
+7
View File
@@ -21,6 +21,13 @@ func (r *CertDeploy) Rules(ctx http.Context) map[string]string {
}
}
func (r *CertDeploy) Filters(ctx http.Context) map[string]string {
return map[string]string{
"id": "uint",
"website_id": "uint",
}
}
func (r *CertDeploy) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -19,6 +19,10 @@ func (r *CertShowAndDestroy) Rules(ctx http.Context) map[string]string {
}
}
func (r *CertShowAndDestroy) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *CertShowAndDestroy) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+10 -27
View File
@@ -3,7 +3,6 @@ package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/spf13/cast"
)
type CertStore struct {
@@ -11,8 +10,8 @@ type CertStore struct {
Domains []string `form:"domains" json:"domains"`
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
UserID uint `form:"user_id" json:"user_id"`
DNSID *uint `form:"dns_id" json:"dns_id"`
WebsiteID *uint `form:"website_id" json:"website_id"`
DNSID uint `form:"dns_id" json:"dns_id"`
WebsiteID uint `form:"website_id" json:"website_id"`
}
func (r *CertStore) Authorize(ctx http.Context) error {
@@ -30,6 +29,14 @@ func (r *CertStore) Rules(ctx http.Context) map[string]string {
}
}
func (r *CertStore) Filters(ctx http.Context) map[string]string {
return map[string]string{
"user_id": "uint",
"dns_id": "uint",
"website_id": "uint",
}
}
func (r *CertStore) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -39,29 +46,5 @@ func (r *CertStore) Attributes(ctx http.Context) map[string]string {
}
func (r *CertStore) PrepareForValidation(ctx http.Context, data validation.Data) error {
// TODO 由于验证器 filter 标签的问题,暂时这里这样处理
userID, exist := data.Get("user_id")
if exist {
err := data.Set("user_id", cast.ToUint(userID))
if err != nil {
return err
}
}
dnsID, exist := data.Get("dns_id")
if exist {
err := data.Set("dns_id", cast.ToUint(dnsID))
if err != nil {
return err
}
}
websiteID, exist := data.Get("website_id")
if exist {
err := data.Set("website_id", cast.ToUint(websiteID))
if err != nil {
return err
}
}
return nil
}
+10 -27
View File
@@ -3,7 +3,6 @@ package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/spf13/cast"
)
type CertUpdate struct {
@@ -12,8 +11,8 @@ type CertUpdate struct {
Domains []string `form:"domains" json:"domains"`
AutoRenew bool `form:"auto_renew" json:"auto_renew"`
UserID uint `form:"user_id" json:"user_id"`
DNSID *uint `form:"dns_id" json:"dns_id"`
WebsiteID *uint `form:"website_id" json:"website_id"`
DNSID uint `form:"dns_id" json:"dns_id"`
WebsiteID uint `form:"website_id" json:"website_id"`
}
func (r *CertUpdate) Authorize(ctx http.Context) error {
@@ -32,6 +31,14 @@ func (r *CertUpdate) Rules(ctx http.Context) map[string]string {
}
}
func (r *CertUpdate) Filters(ctx http.Context) map[string]string {
return map[string]string{
"user_id": "uint",
"dns_id": "uint",
"website_id": "uint",
}
}
func (r *CertUpdate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -41,29 +48,5 @@ func (r *CertUpdate) Attributes(ctx http.Context) map[string]string {
}
func (r *CertUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error {
// TODO 由于验证器 filter 标签的问题,暂时这里这样处理
userID, exist := data.Get("user_id")
if exist {
err := data.Set("user_id", cast.ToUint(userID))
if err != nil {
return err
}
}
dnsID, exist := data.Get("dns_id")
if exist {
err := data.Set("dns_id", cast.ToUint(dnsID))
if err != nil {
return err
}
}
websiteID, exist := data.Get("website_id")
if exist {
err := data.Set("website_id", cast.ToUint(websiteID))
if err != nil {
return err
}
}
return nil
}
@@ -19,6 +19,10 @@ func (r *DNSShowAndDestroy) Rules(ctx http.Context) map[string]string {
}
}
func (r *DNSShowAndDestroy) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *DNSShowAndDestroy) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+8 -5
View File
@@ -4,7 +4,7 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"panel/pkg/acme"
"github.com/TheTNB/panel/pkg/acme"
)
type DNSStore struct {
@@ -19,18 +19,21 @@ func (r *DNSStore) Authorize(ctx http.Context) error {
func (r *DNSStore) Rules(ctx http.Context) map[string]string {
return map[string]string{
"type": "required|in:dnspod,aliyun,cloudflare",
"type": "required|in:dnspod,tencent,aliyun,cloudflare",
"name": "required",
"data": "required",
"data.id": "required_if:type,dnspod",
"data.token": "required_if:type,dnspod",
"data.access_key": "required_if:type,aliyun",
"data.secret_key": "required_if:type,aliyun",
"data.email": "required_if:type,cloudflare",
"data.access_key": "required_if:type,aliyun,tencent",
"data.secret_key": "required_if:type,aliyun,tencent",
"data.api_key": "required_if:type,cloudflare",
}
}
func (r *DNSStore) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *DNSStore) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+5 -1
View File
@@ -4,7 +4,7 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"panel/pkg/acme"
"github.com/TheTNB/panel/pkg/acme"
)
type DNSUpdate struct {
@@ -33,6 +33,10 @@ func (r *DNSUpdate) Rules(ctx http.Context) map[string]string {
}
}
func (r *DNSUpdate) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *DNSUpdate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -19,6 +19,10 @@ func (r *Obtain) Rules(ctx http.Context) map[string]string {
}
}
func (r *Obtain) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Obtain) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+4
View File
@@ -19,6 +19,10 @@ func (r *Renew) Rules(ctx http.Context) map[string]string {
}
}
func (r *Renew) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Renew) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
@@ -19,6 +19,10 @@ func (r *UserShowAndDestroy) Rules(ctx http.Context) map[string]string {
}
}
func (r *UserShowAndDestroy) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UserShowAndDestroy) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+6 -2
View File
@@ -21,12 +21,16 @@ func (r *UserStore) Rules(ctx http.Context) map[string]string {
return map[string]string{
"ca": "required|in:letsencrypt,zerossl,sslcom,google,buypass",
"email": "required|email",
"kid": "required_unless:ca,letsencrypt,buypass",
"hmac_encoded": "required_unless:ca,letsencrypt,buypass",
"kid": "required_if:ca,sslcom,google",
"hmac_encoded": "required_if:ca,sslcom,google",
"key_type": "required|in:P256,P384,2048,4096",
}
}
func (r *UserStore) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UserStore) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+6 -2
View File
@@ -23,12 +23,16 @@ func (r *UserUpdate) Rules(ctx http.Context) map[string]string {
"id": "required|uint|min:1|exists:cert_users,id",
"ca": "required|in:letsencrypt,zerossl,sslcom,google,buypass",
"email": "required|email",
"kid": "required_unless:ca,letsencrypt,buypass",
"hmac_encoded": "required_unless:ca,letsencrypt,buypass",
"kid": "required_if:ca,sslcom,google",
"hmac_encoded": "required_if:ca,sslcom,google",
"key_type": "required|in:P256,P384,2048,4096",
}
}
func (r *UserUpdate) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *UserUpdate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
+10 -10
View File
@@ -16,20 +16,20 @@ func (r *Paginate) Authorize(ctx http.Context) error {
func (r *Paginate) Rules(ctx http.Context) map[string]string {
return map[string]string{
"page": "required|uint|min:1",
"limit": "required|uint|min:1",
"page": "required|int|min:1",
"limit": "required|int|min:1",
}
}
func (r *Paginate) Filters(ctx http.Context) map[string]string {
return map[string]string{
"page": "int",
"limit": "int",
}
}
func (r *Paginate) Messages(ctx http.Context) map[string]string {
return map[string]string{
"page.required": "分页参数 page 不能为空",
"page.uint": "分页参数 page 必须是一个整数",
"page.min": "分页参数 page 必须大于等于 1",
"limit.required": "分页参数 limit 不能为空",
"limit.uint": "分页参数 limit 必须是一个整数",
"limit.min": "分页参数 limit 必须大于等于 1",
}
return map[string]string{}
}
func (r *Paginate) Attributes(ctx http.Context) map[string]string {
@@ -0,0 +1,85 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/TheTNB/panel/pkg/types"
)
type ContainerCreate struct {
Name string `form:"name" json:"name"`
Image string `form:"image" json:"image"`
Ports []types.ContainerPort `form:"ports" json:"ports"`
Network string `form:"network" json:"network"`
Volumes []types.ContainerVolume `form:"volumes" json:"volumes"`
Labels []types.KV `form:"labels" json:"labels"`
Env []types.KV `form:"env" json:"env"`
Entrypoint []string `form:"entrypoint" json:"entrypoint"`
Command []string `form:"command" json:"command"`
RestartPolicy string `form:"restart_policy" json:"restart_policy"`
AutoRemove bool `form:"auto_remove" json:"auto_remove"`
Privileged bool `form:"privileged" json:"privileged"`
OpenStdin bool `form:"openStdin" json:"open_stdin"`
PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"`
Tty bool `form:"tty" json:"tty"`
CPUShares int64 `form:"cpu_shares" json:"cpu_shares"`
CPUs int64 `form:"cpus" json:"cpus"`
Memory int64 `form:"memory" json:"memory"`
}
func (r *ContainerCreate) Authorize(ctx http.Context) error {
return nil
}
func (r *ContainerCreate) Rules(ctx http.Context) map[string]string {
return map[string]string{
"name": "required|string",
"image": "required|string",
"ports": "slice",
/*"ports.*.host": "string",
"ports.*.host_start": "int",
"ports.*.host_end": "int",
"ports.*.container_start": "int",
"ports.*.container_end": "int",
"ports.*.protocol": "string|in:tcp,udp",*/
"network": "string",
"volumes": "slice",
/*"volumes.*.host": "string",
"volumes.*.container": "string",
"volumes.*.mode": "string|in:ro,rw",*/
"labels": "slice",
"env": "slice",
"entrypoint": "slice",
"command": "slice",
"restart_policy": "string|in:always,on-failure,unless-stopped,no",
"auto_remove": "bool",
"privileged": "bool",
"open_stdin": "bool",
"publish_all_ports": "bool",
"tty": "bool",
"cpu_shares": "int",
"cpus": "int",
"memory": "int",
}
}
func (r *ContainerCreate) Filters(ctx http.Context) map[string]string {
return map[string]string{
"cpu_shares": "int",
"cpus": "int",
"memory": "int",
}
}
func (r *ContainerCreate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ContainerCreate) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ContainerCreate) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
@@ -0,0 +1,38 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type ContainerRename struct {
ID string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
}
func (r *ContainerRename) Authorize(ctx http.Context) error {
return nil
}
func (r *ContainerRename) Rules(ctx http.Context) map[string]string {
return map[string]string{
"id": "required|string",
"name": "required|string",
}
}
func (r *ContainerRename) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ContainerRename) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ContainerRename) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ContainerRename) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
@@ -0,0 +1,74 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/TheTNB/panel/pkg/types"
)
type ContainerUpdate struct {
ID string `form:"id" json:"id"`
Name string `form:"name" json:"name"`
Image string `form:"image" json:"image"`
Ports []types.ContainerPort `form:"ports" json:"ports"`
Network string `form:"network" json:"network"`
Volumes []types.ContainerVolume `form:"volumes" json:"volumes"`
Labels []string `form:"labels" json:"labels"`
Env []string `form:"env" json:"env"`
Entrypoint []string `form:"entrypoint" json:"entrypoint"`
Command []string `form:"command" json:"command"`
RestartPolicy string `form:"restart_policy" json:"restart_policy"`
AutoRemove bool `form:"auto_remove" json:"auto_remove"`
Privileged bool `form:"privileged" json:"privileged"`
OpenStdin bool `form:"openStdin" json:"open_stdin"`
PublishAllPorts bool `form:"publish_all_ports" json:"publish_all_ports"`
Tty bool `form:"tty" json:"tty"`
CPUShares int64 `form:"cpu_shares" json:"cpu_shares"`
CPUs int64 `form:"cpus" json:"cpus"`
Memory int64 `form:"memory" json:"memory"`
}
func (r *ContainerUpdate) Authorize(ctx http.Context) error {
return nil
}
func (r *ContainerUpdate) Rules(ctx http.Context) map[string]string {
return map[string]string{
"id": "required|string",
"name": "required|string",
"image": "required|string",
"ports": "slice",
"network": "string",
"volumes": "slice",
"labels": "slice",
"env": "slice",
"entrypoint": "slice",
"command": "slice",
"restart_policy": "string|in:always,on-failure,unless-stopped,no",
"auto_remove": "bool",
"privileged": "bool",
"open_stdin": "bool",
"publish_all_ports": "bool",
"tty": "bool",
"cpu_shares": "int",
"cpus": "int",
"memory": "int",
}
}
func (r *ContainerUpdate) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ContainerUpdate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ContainerUpdate) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ContainerUpdate) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+36
View File
@@ -0,0 +1,36 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type ID struct {
ID string `form:"id" json:"id"`
}
func (r *ID) Authorize(ctx http.Context) error {
return nil
}
func (r *ID) Rules(ctx http.Context) map[string]string {
return map[string]string{
"id": "required|string",
}
}
func (r *ID) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ID) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ID) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ID) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+42
View File
@@ -0,0 +1,42 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type ImagePull struct {
Name string `form:"name" json:"name"`
Auth bool `form:"auth" json:"auth"`
Username string `form:"username" json:"username"`
Password string `form:"password" json:"password"`
}
func (r *ImagePull) Authorize(ctx http.Context) error {
return nil
}
func (r *ImagePull) Rules(ctx http.Context) map[string]string {
return map[string]string{
"name": "required|string",
"auth": "bool",
"username": "string",
"password": "string",
}
}
func (r *ImagePull) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ImagePull) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ImagePull) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *ImagePull) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
@@ -0,0 +1,38 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type NetworkConnectDisConnect struct {
Network string `form:"network" json:"network"`
Container string `form:"container" json:"container"`
}
func (r *NetworkConnectDisConnect) Authorize(ctx http.Context) error {
return nil
}
func (r *NetworkConnectDisConnect) Rules(ctx http.Context) map[string]string {
return map[string]string{
"network": "required|string",
"container": "required|string",
}
}
func (r *NetworkConnectDisConnect) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NetworkConnectDisConnect) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NetworkConnectDisConnect) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NetworkConnectDisConnect) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
@@ -0,0 +1,48 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/TheTNB/panel/pkg/types"
)
type NetworkCreate struct {
Name string `form:"name" json:"name"`
Driver string `form:"driver" json:"driver"`
Ipv4 types.ContainerNetwork `form:"ipv4" json:"ipv4"`
Ipv6 types.ContainerNetwork `form:"ipv6" json:"ipv6"`
Labels []types.KV `form:"labels" json:"labels"`
Options []types.KV `form:"options" json:"options"`
}
func (r *NetworkCreate) Authorize(ctx http.Context) error {
return nil
}
func (r *NetworkCreate) Rules(ctx http.Context) map[string]string {
return map[string]string{
"name": "required|string",
"driver": "required|string|in:bridge,overlay,macvlan,ipvlan",
"ipv4": "required",
"ipv6": "required",
"labels": "slice",
"options": "slice",
}
}
func (r *NetworkCreate) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NetworkCreate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NetworkCreate) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NetworkCreate) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
@@ -0,0 +1,44 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"github.com/TheTNB/panel/pkg/types"
)
type VolumeCreate struct {
Name string `form:"name" json:"name"`
Driver string `form:"driver" json:"driver"`
Labels []types.KV `form:"labels" json:"labels"`
Options []types.KV `form:"options" json:"options"`
}
func (r *VolumeCreate) Authorize(ctx http.Context) error {
return nil
}
func (r *VolumeCreate) Rules(ctx http.Context) map[string]string {
return map[string]string{
"name": "required|string",
"driver": "required|string|in:local",
"labels": "slice",
"options": "slice",
}
}
func (r *VolumeCreate) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *VolumeCreate) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *VolumeCreate) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *VolumeCreate) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+39
View File
@@ -0,0 +1,39 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type Archive struct {
Paths []string `form:"paths" json:"paths"`
File string `form:"file" json:"file"`
}
func (r *Archive) Authorize(ctx http.Context) error {
return nil
}
func (r *Archive) Rules(ctx http.Context) map[string]string {
return map[string]string{
"paths": "array",
"paths.*": `regex:^/.*$|path_exists`,
"file": `regex:^/.*$|path_not_exists`,
}
}
func (r *Archive) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Archive) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Archive) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Archive) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+38
View File
@@ -0,0 +1,38 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type Copy struct {
Source string `form:"source" json:"source"`
Target string `form:"target" json:"target"`
}
func (r *Copy) Authorize(ctx http.Context) error {
return nil
}
func (r *Copy) Rules(ctx http.Context) map[string]string {
return map[string]string{
"source": `regex:^/.*$|path_exists`,
"target": `regex:^/.*$`,
}
}
func (r *Copy) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Copy) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Copy) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Copy) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+36
View File
@@ -0,0 +1,36 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type Exist struct {
Path string `form:"path" json:"path"`
}
func (r *Exist) Authorize(ctx http.Context) error {
return nil
}
func (r *Exist) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": `regex:^/.*$|path_exists`,
}
}
func (r *Exist) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Exist) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Exist) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Exist) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+38
View File
@@ -0,0 +1,38 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type Move struct {
Source string `form:"source" json:"source"`
Target string `form:"target" json:"target"`
}
func (r *Move) Authorize(ctx http.Context) error {
return nil
}
func (r *Move) Rules(ctx http.Context) map[string]string {
return map[string]string{
"source": `regex:^/.*$|path_exists`,
"target": `regex:^/.*$`,
}
}
func (r *Move) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Move) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Move) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Move) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+36
View File
@@ -0,0 +1,36 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type NotExist struct {
Path string `form:"path" json:"path"`
}
func (r *NotExist) Authorize(ctx http.Context) error {
return nil
}
func (r *NotExist) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": `regex:^/.*$|path_not_exists`,
}
}
func (r *NotExist) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NotExist) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NotExist) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *NotExist) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+42
View File
@@ -0,0 +1,42 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type Permission struct {
Path string `form:"path" json:"path"`
Mode string `form:"mode" json:"mode"`
Owner string `form:"owner" json:"owner"`
Group string `form:"group" json:"group"`
}
func (r *Permission) Authorize(ctx http.Context) error {
return nil
}
func (r *Permission) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": `regex:^/.*$|path_exists`,
"mode": "regex:^0[0-7]{3}$",
"owner": "regex:^[a-zA-Z0-9_-]+$",
"group": "regex:^[a-zA-Z0-9_-]+$",
}
}
func (r *Permission) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Permission) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Permission) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Permission) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+38
View File
@@ -0,0 +1,38 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type Save struct {
Path string `form:"path" json:"path"`
Content string `form:"content" json:"content"`
}
func (r *Save) Authorize(ctx http.Context) error {
return nil
}
func (r *Save) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": `regex:^/.*$|path_exists`,
"content": "required|string",
}
}
func (r *Save) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Save) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Save) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Save) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+38
View File
@@ -0,0 +1,38 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type Search struct {
Path string `form:"path" json:"path"`
KeyWord string `form:"keyword" json:"keyword"`
}
func (r *Search) Authorize(ctx http.Context) error {
return nil
}
func (r *Search) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": `regex:^/.*$|path_exists`,
"keyword": "required|string",
}
}
func (r *Search) Filters(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Search) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Search) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Search) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}

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