Compare commits

...

241 Commits

Author SHA1 Message Date
耗子 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
192 changed files with 11330 additions and 6303 deletions
+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']
+3 -3
View File
@@ -16,7 +16,7 @@ jobs:
- 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"' -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 }}
+2 -2
View File
@@ -11,13 +11,13 @@ jobs:
- uses: actions/checkout@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 }}
+2 -2
View File
@@ -16,11 +16,11 @@ jobs:
- name: Set up Go
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
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
rm -rf public
unzip frontend.zip
mv dist public
+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)
+2 -4
View File
@@ -14,13 +14,11 @@ jobs:
- uses: actions/checkout@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 ./...
+2 -2
View File
@@ -11,13 +11,13 @@ jobs:
- uses: actions/checkout@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
+8 -12
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,7 +38,7 @@ 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 ./...
@@ -54,18 +48,18 @@ 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"' -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/mirror.sjtu.edu.cn/g' /etc/apk/repositories
- apk add --no-cache curl jq unzip zip
script:
- 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
@@ -93,4 +87,6 @@ release:
# generate a changelog.
GIT_DEPTH: 0
script:
- sed -i 's/dl-cdn.alpinelinux.org/mirror.sjtu.edu.cn/g' /etc/apk/repositories
- apk add --no-cache upx
- goreleaser release --clean
+20 -10
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"
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: true
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://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: false
use_package_registry: true
# Set this if you set GITLAB_TOKEN to the value of CI_JOB_TOKEN.
#
# Since: v1.11
+1 -1
View File
@@ -1,6 +1,6 @@
## 行为准则
耗子Linux面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
耗子 Linux 面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
- 参与者将容忍反对意见。
- 参与者必须确保他们的语言和行为没有人身攻击和贬低个人言论。
+33 -38
View File
@@ -5,12 +5,11 @@
<h1 align="center">耗子 Linux 面板</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://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>
耗子 Linux 面板是使用 Golang + Vue 开发的轻量 Linux 服务器运维管理面板,以 GNU Affero General Public License v3.0 开源。
@@ -24,7 +23,7 @@
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障。
4. **高效运维:** 面板 UI 界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
5. **离线运行:** 面板运行可不依赖任何外部服务,你甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 1 年无事故。
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 1 年以上无事故。
7. **开源开放:** 面板开源,你可以自由修改、审计面板源码,安全性有保障。
## UI 截图
@@ -33,13 +32,13 @@
## 运行环境
耗子Linux面板仅支持 `amd64` | `arm64` 架构下的主流系统的最新版本,不支持 `Ubuntu`,因为其发版太过频繁,难以维护。
耗子 Linux 面板仅支持 `amd64` | `arm64` 架构下的主流系统的最新版本,不支持 `Ubuntu`,因为其发版太过频繁,难以维护。
低配机器建议使用 `Debian`,资源占用较 `RHEL` 系更低。其他机器建议使用 `AlmaLinux` | `RockyLinux`,维护周期更长也更稳定。
不在下表中的其他 RHEL 9.x 系统,可自行尝试安装,但不保证能够正常运行,且不提供无偿技术支持(理论上不会有大问题)。
CentOS Stream 可使用迁移脚本迁移至支持的系统: [CentOS 8/9 迁移脚本](https://github.com/haozi-team/byecentos)
CentOS Stream 可使用迁移脚本迁移至支持的系统: [CentOS 8/9 迁移脚本](https://github.com/TheTNB/byecentos)
| 系统 | 版本 |
|------------|----|
@@ -53,12 +52,12 @@ CentOS Stream 可使用迁移脚本迁移至支持的系统: [CentOS 8/9 迁移
## 安装面板
> **Warning**
> 安装面板前,你需要了解LNMP环境的基本知识,以及如何处理常见的LNMP环境问题,不建议0基础的用户安装和使用耗子Linux面板。
> 安装面板前,你需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,不建议 0 基础的用户安装和使用耗子 Linux 面板。
如果你决定继续,请以`root`用户登录服务器,执行以下命令安装面板:
```shell
HAOZI_DL_URL="https://git.haozi.net/opensource/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 验证失败,文件可能被篡改,已终止操作"
```
## 卸载面板
@@ -68,7 +67,7 @@ HAOZI_DL_URL="https://git.haozi.net/opensource/download/-/raw/main/panel"; curl
如果你无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
```shell
HAOZI_DL_URL="https://git.haozi.net/opensource/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 验证失败,文件可能被篡改,已终止操作"
```
卸载面板前请务必备份好所有数据,提前卸载面板全部插件。卸载后数据将**无法恢复**!
@@ -81,54 +80,50 @@ HAOZI_DL_URL="https://git.haozi.net/opensource/download/-/raw/main/panel"; curl
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://git.haozi.net/opensource/panel](https://git.haozi.net/opensource/panel),勿在 GitHub 上提交;
- 当 PR 开发完毕后,请为其添加 `🚀 Review Ready` 标签,维护者将及时进行评审;
- 我们非常欢迎您的贡献,将在下次发版时将您添加到首页贡献者中;❤️
## 赞助商
如果耗子 Linux 面板对你有帮助,欢迎[赞助我们](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>
+31 -36
View File
@@ -5,12 +5,11 @@
<h1 align="center">HaoZi Linux 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://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>
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.
@@ -24,7 +23,7 @@ Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ
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 1 year without any accidents.
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
@@ -39,7 +38,7 @@ Recommended to use `Debian` for low-configuration machines, as its resource usag
For other RHEL 9.x systems not in the table, 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)
CentOS Stream can be migrated to a supported system using the migration script: [CentOS 8/9 Migration Script](https://github.com/TheTNB/byecentos)
| OS | Version |
|------------|---------|
@@ -58,7 +57,7 @@ As system versions are constantly updated, we may also terminate support for som
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://git.haozi.net/opensource/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
@@ -68,7 +67,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://git.haozi.net/opensource/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!
@@ -81,54 +80,50 @@ 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://git.haozi.net/opensource/panel](https://git.haozi.net/opensource/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 HaoZi Linux 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>
+2 -2
View File
@@ -1,8 +1,8 @@
## 安全说明
安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子Linux面板,至今无一例安全事故。
安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子 Linux 面板,至今无一例安全事故。
耗子Linux面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证**。
耗子 Linux 面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证**。
如果您在使用面板的过程中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
+11 -3
View File
@@ -1,6 +1,7 @@
package commands
import (
"context"
"crypto/x509"
"encoding/pem"
@@ -10,9 +11,11 @@ import (
"github.com/goravel/framework/support/carbon"
"panel/app/models"
"panel/app/services"
"panel/internal"
"panel/internal/services"
)
// CertRenew 证书续签
type CertRenew struct {
}
@@ -23,7 +26,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 +38,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 internal.Status != internal.StatusNormal {
return nil
}
var certs []models.Cert
err := facades.Orm().Query().With("Website").With("User").With("DNS").Find(&certs)
if err != nil {
+34 -14
View File
@@ -1,6 +1,7 @@
package commands
import (
"context"
"strconv"
"github.com/gookit/color"
@@ -11,10 +12,12 @@ import (
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
)
// Monitoring 系统监控
type Monitoring struct {
}
@@ -25,7 +28,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,39 +40,55 @@ 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 internal.Status != internal.StatusNormal {
return nil
}
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
for _, cpu := range info.Cpus {
cpu.VendorID = ""
cpu.Family = ""
cpu.Model = ""
cpu.PhysicalID = ""
cpu.ModelName = ""
cpu.Flags = nil
}
if internal.Status != internal.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())
color.Redf(translate.Get("commands.panel:monitoring.fail")+": %s", err.Error())
return nil
}
// 删除过期数据
err = facades.Orm().Query().Where("key", "monitor_days").First(&setting)
monitorDays := setting.Get(models.SettingKeyMonitorDays)
days, err := strconv.Atoi(monitorDays)
if err != nil {
return nil
}
if setting.Value == "0" || len(setting.Value) == 0 {
return nil
}
days, err := strconv.Atoi(setting.Value)
if err != nil {
if days <= 0 || internal.Status != internal.StatusNormal {
return nil
}
_, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{})
if err != nil {
if _, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{}); err != nil {
facades.Log().Infof("[面板] 系统监控删除过期数据失败: %s", err.Error())
return nil
}
+274 -128
View File
@@ -1,6 +1,7 @@
package commands
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -15,10 +16,12 @@ import (
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
)
// Panel 面板命令行
type Panel struct {
}
@@ -29,7 +32,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 +50,142 @@ 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.Redln(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: "耗子 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")}}
err = facades.Orm().Query().Create(&settings)
if err != nil {
color.Redln("初始化失败")
color.Redln(translate.Get("commands.panel.init.fail"))
return nil
}
hash, err := facades.Hash().Make(tools.RandomString(32))
if err != nil {
color.Redln("初始化失败")
color.Redln(translate.Get("commands.panel.init.fail"))
return nil
}
user := services.NewUserImpl()
_, err = user.Create("admin", hash)
if err != nil {
color.Redln("创建管理员失败")
color.Redln(translate.Get("commands.panel.init.adminFail"))
return nil
}
color.Greenln("初始化成功")
color.Greenln(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("当前有任务正在执行,禁止更新")
color.Redln(translate.Get("commands.panel.update.taskCheck"))
return nil
}
panel, err := tools.GetLatestPanelVersion()
if err != nil {
color.Redln("获取最新版本失败")
color.Redln(translate.Get("commands.panel.update.versionFail"))
return err
}
err = tools.UpdatePanel(panel)
if err != nil {
color.Redln("更新失败: " + err.Error())
internal.Status = internal.StatusUpgrade
if err = tools.UpdatePanel(panel); err != nil {
internal.Status = internal.StatusFailed
color.Redln(translate.Get("commands.panel.update.fail") + ": " + err.Error())
return nil
}
color.Greenln("更新成功")
internal.Status = internal.StatusNormal
color.Greenln(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.Redln(translate.Get("commands.panel.getInfo.adminGetFail"))
return nil
}
password := tools.RandomString(16)
hash, err := facades.Hash().Make(password)
if err != nil {
color.Redln("生成密码失败")
color.Redln(translate.Get("commands.panel.getInfo.passwordGenerationFail"))
return nil
}
user.Username = tools.RandomString(8)
user.Password = hash
if user.Email == "" {
user.Email = tools.RandomString(8) + "@example.com"
}
err = facades.Orm().Query().Save(&user)
if err != nil {
color.Redln("保存管理员信息失败")
color.Redln(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'`)
if err != nil {
color.Redln("获取面板端口失败")
color.Redln(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.Greenln(translate.Get("commands.panel.getInfo.username") + ": " + user.Username)
color.Greenln(translate.Get("commands.panel.getInfo.password") + ": " + password)
color.Greenln(translate.Get("commands.panel.port") + ": " + port)
color.Greenln(translate.Get("commands.panel.entrance") + ": " + facades.Config().GetString("http.entrance"))
color.Greenln(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'`)
if err != nil {
color.Redln("获取面板端口失败")
color.Redln(translate.Get("commands.panel.portFail"))
return nil
}
color.Greenln("面板端口: " + port)
color.Greenln(translate.Get("commands.panel.port") + ": " + port)
case "getEntrance":
color.Greenln("面板入口: " + facades.Config().GetString("http.entrance"))
color.Greenln(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'`)
if err != nil {
color.Redln("获取面板入口失败")
color.Redln(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("删除面板入口失败")
color.Redln(translate.Get("commands.panel.deleteEntrance.fail"))
return nil
}
color.Greenln("删除面板入口成功")
color.Greenln(translate.Get("commands.panel.deleteEntrance.success"))
case "writePlugin":
slug := arg1
version := arg2
if len(slug) == 0 || len(version) == 0 {
color.Redln("参数错误")
color.Redln(translate.Get("commands.panel.writePlugin.paramFail"))
return nil
}
@@ -177,31 +197,31 @@ func (receiver *Panel) Handle(ctx console.Context) error {
})
if err != nil {
color.Redln("写入插件安装状态失败")
color.Redln(translate.Get("commands.panel.writePlugin.fail"))
return nil
}
color.Greenln("写入插件安装状态成功")
color.Greenln(translate.Get("commands.panel.writePlugin.success"))
case "deletePlugin":
slug := arg1
if len(slug) == 0 {
color.Redln("参数错误")
color.Redln(translate.Get("commands.panel.deletePlugin.paramFail"))
return nil
}
_, err := facades.Orm().Query().Where("slug", slug).Delete(&models.Plugin{})
if err != nil {
color.Redln("移除插件安装状态失败")
color.Redln(translate.Get("commands.panel.deletePlugin.fail"))
return nil
}
color.Greenln("移除插件安装状态成功")
color.Greenln(translate.Get("commands.panel.deletePlugin.success"))
case "writeMysqlPassword":
password := arg1
if len(password) == 0 {
color.Redln("参数错误")
color.Redln(translate.Get("commands.panel.writeMysqlPassword.paramFail"))
return nil
}
@@ -213,20 +233,20 @@ func (receiver *Panel) Handle(ctx console.Context) error {
})
if err != nil {
color.Redln("写入MySQL root密码失败")
color.Redln(translate.Get("commands.panel.writeMysqlPassword.fail"))
return nil
}
color.Greenln("写入MySQL root密码成功")
color.Greenln(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.Redln(translate.Get("commands.panel.cleanTask.fail"))
return nil
}
color.Greenln("清理任务成功")
color.Greenln(translate.Get("commands.panel.cleanTask.success"))
case "backup":
backupType := arg1
@@ -235,37 +255,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.Redln(translate.Get("commands.panel.backup.paramFail"))
return nil
}
color.Greenln(hr)
color.Greenln("★ 开始备份 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln("★ " + translate.Get("commands.panel.backup.start") + " [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
if !tools.Exists(path) {
if err := tools.Mkdir(path, 0644); err != nil {
color.Redln("|-创建备份目录失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.backupDirFail") + ": " + err.Error())
return nil
}
}
switch backupType {
case "website":
color.Yellowln("|-目标网站: " + name)
color.Yellowln("|-" + 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.Redln("|-" + translate.Get("commands.panel.backup.siteNotExist"))
color.Greenln(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())
color.Redln("|-" + translate.Get("commands.panel.backup.backupFail") + ": " + err.Error())
return nil
}
color.Greenln("|-备份成功")
color.Greenln("|-" + translate.Get("commands.panel.backup.backupSuccess"))
case "mysql":
rootPassword := services.NewSettingImpl().Get(models.SettingKeyMysqlRootPassword)
@@ -273,81 +293,81 @@ func (receiver *Panel) Handle(ctx console.Context) error {
err := os.Setenv("MYSQL_PWD", rootPassword)
if err != nil {
color.Redln("|-备份MySQL数据库失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.mysqlBackupFail") + ": " + err.Error())
color.Greenln(hr)
return nil
}
color.Greenln("|-目标MySQL数据库: " + name)
color.Greenln("|-开始导出")
color.Greenln("|-" + translate.Get("commands.panel.backup.targetMysql") + ": " + name)
color.Greenln("|-" + translate.Get("commands.panel.backup.startExport"))
if _, err = tools.Exec(`mysqldump -uroot ` + name + ` > /tmp/` + backupFile + ` 2>&1`); err != nil {
color.Redln("|-导出失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.exportFail") + ": " + err.Error())
return nil
}
color.Greenln("|-导出成功")
color.Greenln("|-开始压缩")
color.Greenln("|-" + translate.Get("commands.panel.backup.exportSuccess"))
color.Greenln("|-" + translate.Get("commands.panel.backup.startCompress"))
if _, err = tools.Exec("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil {
color.Redln("|-压缩失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error())
return nil
}
if err := tools.Remove("/tmp/" + backupFile); err != nil {
color.Redln("|-删除失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.deleteFail") + ": " + err.Error())
return nil
}
color.Greenln("|-压缩成功")
color.Greenln("|-开始移动")
color.Greenln("|-" + translate.Get("commands.panel.backup.compressSuccess"))
color.Greenln("|-" + translate.Get("commands.panel.backup.startMove"))
if err := tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
color.Redln("|-移动失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.moveFail") + ": " + err.Error())
return nil
}
color.Greenln("|-移动成功")
color.Greenln("|-" + translate.Get("commands.panel.backup.moveSuccess"))
_ = os.Unsetenv("MYSQL_PWD")
color.Greenln("|-备份成功")
color.Greenln("|-" + 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`)
if err != nil {
color.Redln("|-获取数据库失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.databaseGetFail") + ": " + err.Error())
color.Greenln(hr)
return nil
}
if strings.Contains(check, name) {
color.Redln("|-数据库不存在")
if !strings.Contains(check, name) {
color.Redln("|-" + translate.Get("commands.panel.backup.databaseNotExist"))
color.Greenln(hr)
return nil
}
color.Greenln("|-目标PostgreSQL数据库: " + name)
color.Greenln("|-开始导出")
color.Greenln("|-" + translate.Get("commands.panel.backup.targetPostgres") + ": " + name)
color.Greenln("|-" + translate.Get("commands.panel.backup.startExport"))
if _, err = tools.Exec(`su - postgres -c "pg_dump '` + name + `'" > /tmp/` + backupFile + ` 2>&1`); err != nil {
color.Redln("|-导出失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.exportFail") + ": " + err.Error())
return nil
}
color.Greenln("|-导出成功")
color.Greenln("|-开始压缩")
color.Greenln("|-" + translate.Get("commands.panel.backup.exportSuccess"))
color.Greenln("|-" + translate.Get("commands.panel.backup.startCompress"))
if _, err = tools.Exec("cd /tmp && zip -r " + backupFile + ".zip " + backupFile); err != nil {
color.Redln("|-压缩失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.compressFail") + ": " + err.Error())
return nil
}
if err := tools.Remove("/tmp/" + backupFile); err != nil {
color.Redln("|-删除失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.deleteFail") + ": " + err.Error())
return nil
}
color.Greenln("|-压缩成功")
color.Greenln("|-开始移动")
color.Greenln("|-" + translate.Get("commands.panel.backup.compressSuccess"))
color.Greenln("|-" + translate.Get("commands.panel.backup.startMove"))
if err := tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
color.Redln("|-移动失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.moveFail") + ": " + err.Error())
return nil
}
color.Greenln("|-移动成功")
color.Greenln("|-备份成功")
color.Greenln("|-" + translate.Get("commands.panel.backup.moveSuccess"))
color.Greenln("|-" + translate.Get("commands.panel.backup.success"))
}
color.Greenln(hr)
files, err := os.ReadDir(path)
if err != nil {
color.Redln("|-清理失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.cleanupFail") + ": " + err.Error())
return nil
}
var filteredFiles []os.FileInfo
@@ -365,15 +385,15 @@ 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)
color.Yellowln("|-" + translate.Get("commands.panel.backup.cleanBackup") + ": " + fileToDelete)
if err := tools.Remove(fileToDelete); err != nil {
color.Redln("|-清理失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.backup.cleanupFail") + ": " + err.Error())
return nil
}
}
color.Greenln("|-清理完成")
color.Greenln("|-" + translate.Get("commands.panel.backup.cleanupSuccess"))
color.Greenln(hr)
color.Greenln("☆ 备份完成 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln("☆ " + translate.Get("commands.panel.backup.success") + " [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
case "cutoff":
@@ -381,44 +401,44 @@ func (receiver *Panel) Handle(ctx console.Context) error {
save := arg2
hr := `+----------------------------------------------------`
if len(name) == 0 || len(save) == 0 {
color.Redln("参数错误")
color.Redln(translate.Get("commands.panel.cutoff.paramFail"))
return nil
}
color.Greenln(hr)
color.Greenln("★ 开始切割 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln("★ " + translate.Get("commands.panel.cutoff.start") + " [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
color.Yellowln("|-目标网站: " + name)
color.Yellowln("|-" + 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.Redln("|-" + translate.Get("commands.panel.cutoff.siteNotExist"))
color.Greenln(hr)
return nil
}
logPath := "/www/wwwlogs/" + website.Name + ".log"
if !tools.Exists(logPath) {
color.Redln("|-日志文件不存在")
color.Redln("|-" + translate.Get("commands.panel.cutoff.logNotExist"))
color.Greenln(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())
color.Redln("|-" + translate.Get("commands.panel.cutoff.backupFail") + ": " + err.Error())
return nil
}
if _, err := tools.Exec(`echo "" > ` + logPath); err != nil {
color.Redln("|-清空失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.cutoff.clearFail") + ": " + err.Error())
return nil
}
color.Greenln("|-切割成功")
color.Greenln("|-" + translate.Get("commands.panel.cutoff.cutSuccess"))
color.Greenln(hr)
files, err := os.ReadDir("/www/wwwlogs")
if err != nil {
color.Redln("|-清理失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.cutoff.cleanupFail") + ": " + err.Error())
return nil
}
var filteredFiles []os.FileInfo
@@ -436,15 +456,15 @@ 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)
color.Yellowln("|-" + translate.Get("commands.panel.cutoff.clearLog") + ": " + fileToDelete)
if err := tools.Remove(fileToDelete); err != nil {
color.Redln("|-清理失败: " + err.Error())
color.Redln("|-" + translate.Get("commands.panel.cutoff.cleanupFail") + ": " + err.Error())
return nil
}
}
color.Greenln("|-清理完成")
color.Greenln("|-" + translate.Get("commands.panel.cutoff.cleanupSuccess"))
color.Greenln(hr)
color.Greenln("☆ 切割完成 [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln("☆ " + translate.Get("commands.panel.cutoff.end") + " [" + carbon.Now().ToDateTimeString() + "]")
color.Greenln(hr)
case "writeSite":
@@ -454,19 +474,19 @@ func (receiver *Panel) Handle(ctx console.Context) error {
php := cast.ToInt(arg4)
ssl := cast.ToBool(ctx.Argument(5))
if len(name) == 0 || len(path) == 0 {
color.Redln("参数错误")
color.Redln(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.Redln(translate.Get("commands.panel.writeSite.siteExist"))
return nil
}
_, err := os.Stat(path)
if os.IsNotExist(err) {
color.Redln("网站目录不存在")
color.Redln(translate.Get("commands.panel.writeSite.pathNotExist"))
return nil
}
@@ -478,32 +498,32 @@ func (receiver *Panel) Handle(ctx console.Context) error {
Ssl: ssl,
})
if err != nil {
color.Redln("写入网站失败")
color.Redln(translate.Get("commands.panel.writeSite.fail"))
return nil
}
color.Greenln("写入网站成功")
color.Greenln(translate.Get("commands.panel.writeSite.success"))
case "deleteSite":
name := arg1
if len(name) == 0 {
color.Redln("参数错误")
color.Redln(translate.Get("commands.panel.deleteSite.paramFail"))
return nil
}
_, err := facades.Orm().Query().Where("name", name).Delete(&models.Website{})
if err != nil {
color.Redln("删除网站失败")
color.Redln(translate.Get("commands.panel.deleteSite.fail"))
return nil
}
color.Greenln("删除网站成功")
color.Greenln(translate.Get("commands.panel.deleteSite.success"))
case "writeSetting":
key := arg1
value := arg2
if len(key) == 0 || len(value) == 0 {
color.Redln("参数错误")
color.Redln(translate.Get("commands.panel.writeSetting.paramFail"))
return nil
}
@@ -514,16 +534,16 @@ func (receiver *Panel) Handle(ctx console.Context) error {
Value: value,
})
if err != nil {
color.Redln("写入设置失败")
color.Redln(translate.Get("commands.panel.writeSetting.fail"))
return nil
}
color.Greenln("写入设置成功")
color.Greenln(translate.Get("commands.panel.writeSetting.success"))
case "getSetting":
key := arg1
if len(key) == 0 {
color.Redln("参数错误")
color.Redln(translate.Get("commands.panel.getSetting.paramFail"))
return nil
}
@@ -537,39 +557,165 @@ func (receiver *Panel) Handle(ctx console.Context) error {
case "deleteSetting":
key := arg1
if len(key) == 0 {
color.Redln("参数错误")
color.Redln(translate.Get("commands.panel.deleteSetting.paramFail"))
return nil
}
_, err := facades.Orm().Query().Where("key", key).Delete(&models.Setting{})
if err != nil {
color.Redln("删除设置失败")
color.Redln(translate.Get("commands.panel.deleteSetting.fail"))
return nil
}
color.Greenln("删除设置成功")
color.Greenln(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.Redln(translate.Get("commands.panel.addSite.paramFail"))
return nil
}
domains := strings.Split(domain, ",")
ports := strings.Split(port, ",")
if len(domains) == 0 || len(ports) == 0 {
color.Redln(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.Redln(err.Error())
return nil
}
if id != 0 {
color.Redln(translate.Get("commands.panel.addSite.siteExist"))
return nil
}
_, err = website.Add(internal.PanelWebsite{
Name: name,
Status: true,
Domains: domains,
Ports: uintPorts,
Path: path,
Php: php,
Ssl: false,
Db: false,
})
if err != nil {
color.Redln(err.Error())
return nil
}
color.Greenln(translate.Get("commands.panel.addSite.success"))
case "removeSite":
name := arg1
if len(name) == 0 {
color.Redln(translate.Get("commands.panel.removeSite.paramFail"))
return nil
}
website := services.NewWebsiteImpl()
id, err := website.GetIDByName(name)
if err != nil {
color.Redln(err.Error())
return nil
}
if id == 0 {
color.Redln(translate.Get("commands.panel.removeSite.siteNotExist"))
return nil
}
if err = website.Delete(id); err != nil {
color.Redln(err.Error())
return nil
}
color.Greenln(translate.Get("commands.panel.removeSite.success"))
case "installPlugin":
slug := arg1
if len(slug) == 0 {
color.Redln(translate.Get("commands.panel.installPlugin.paramFail"))
return nil
}
plugin := services.NewPluginImpl()
if err := plugin.Install(slug); err != nil {
color.Redln(err.Error())
return nil
}
color.Greenln(translate.Get("commands.panel.installPlugin.success"))
case "uninstallPlugin":
slug := arg1
if len(slug) == 0 {
color.Redln(translate.Get("commands.panel.uninstallPlugin.paramFail"))
return nil
}
plugin := services.NewPluginImpl()
if err := plugin.Uninstall(slug); err != nil {
color.Redln(err.Error())
return nil
}
color.Greenln(translate.Get("commands.panel.uninstallPlugin.success"))
case "updatePlugin":
slug := arg1
if len(slug) == 0 {
color.Redln(translate.Get("commands.panel.updatePlugin.paramFail"))
return nil
}
plugin := services.NewPluginImpl()
if err := plugin.Update(slug); err != nil {
color.Redln(err.Error())
return nil
}
color.Greenln(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.Yellowln(facades.Config().GetString("panel.name") + " - " + translate.Get("commands.panel.tool") + " - " + facades.Config().GetString("panel.version"))
color.Greenln(translate.Get("commands.panel.use") + "")
color.Greenln("panel update " + translate.Get("commands.panel.update.description"))
color.Greenln("panel getInfo " + translate.Get("commands.panel.getInfo.description"))
color.Greenln("panel getPort " + translate.Get("commands.panel.getPort.description"))
color.Greenln("panel getEntrance " + translate.Get("commands.panel.getEntrance.description"))
color.Greenln("panel deleteEntrance " + translate.Get("commands.panel.deleteEntrance.description"))
color.Greenln("panel cleanTask " + translate.Get("commands.panel.cleanTask.description"))
color.Greenln("panel backup {website/mysql/postgresql} {name} {path} {save_copies} " + translate.Get("commands.panel.backup.description"))
color.Greenln("panel cutoff {website_name} {save_copies} " + translate.Get("commands.panel.cutoff.description"))
color.Greenln("panel installPlugin {slug} " + translate.Get("commands.panel.installPlugin.description"))
color.Greenln("panel uninstallPlugin {slug} " + translate.Get("commands.panel.uninstallPlugin.description"))
color.Greenln("panel updatePlugin {slug} " + translate.Get("commands.panel.updatePlugin.description"))
color.Greenln("panel addSite {name} {domain} {port} {path} {php} " + translate.Get("commands.panel.addSite.description"))
color.Greenln("panel removeSite {name} " + translate.Get("commands.panel.removeSite.description"))
color.Redln(translate.Get("commands.panel.forDeveloper") + ":")
color.Yellowln("panel init " + translate.Get("commands.panel.init.description"))
color.Yellowln("panel writePlugin {slug} {version} " + translate.Get("commands.panel.writePlugin.description"))
color.Yellowln("panel deletePlugin {slug} " + translate.Get("commands.panel.deletePlugin.description"))
color.Yellowln("panel writeMysqlPassword {password} " + translate.Get("commands.panel.writeMysqlPassword.description"))
color.Yellowln("panel writeSite {name} {status} {path} {php} {ssl} " + translate.Get("commands.panel.writeSite.description"))
color.Yellowln("panel deleteSite {name} " + translate.Get("commands.panel.deleteSite.description"))
color.Yellowln("panel getSetting {name} " + translate.Get("commands.panel.getSetting.description"))
color.Yellowln("panel writeSetting {name} {value} " + translate.Get("commands.panel.writeSetting.description"))
color.Yellowln("panel deleteSetting {name} " + translate.Get("commands.panel.deleteSetting.description"))
}
return nil
+72
View File
@@ -0,0 +1,72 @@
package commands
import (
"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/internal"
"panel/pkg/tools"
)
// 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 {
internal.Status = internal.StatusMaintain
// 优化数据库
if _, err := facades.Orm().Query().Exec("VACUUM"); err != nil {
internal.Status = internal.StatusFailed
facades.Log().Tags("面板", "每日任务").
With(map[string]any{
"error": err.Error(),
}).Error("优化面板数据库失败")
return err
}
// 备份面板
if err := tools.Archive([]string{"/www/panel"}, "/www/backup/panel/panel-"+carbon.Now().ToShortDateTimeString()+".zip"); err != nil {
internal.Status = internal.StatusFailed
facades.Log().Tags("面板", "每日任务").
With(map[string]any{
"error": err.Error(),
}).Error("备份面板失败")
return err
}
// 清理 7 天前的备份
if _, err := tools.Exec(`find /www/backup/panel -mtime +7 -name "*.zip" -exec rm -rf {} \;`); err != nil {
internal.Status = internal.StatusFailed
facades.Log().Tags("面板", "每日任务").
With(map[string]any{
"error": err.Error(),
}).Error("清理面板备份失败")
return err
}
internal.Status = internal.StatusNormal
return nil
}
+3 -1
View File
@@ -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").Daily().SkipIfStillRunning(),
}
}
@@ -23,5 +24,6 @@ func (kernel *Kernel) Commands() []console.Command {
&commands.Panel{},
&commands.Monitoring{},
&commands.CertRenew{},
&commands.PanelTask{},
}
}
+9 -8
View File
@@ -7,12 +7,13 @@ import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
)
type AssetController struct {
setting services.Setting
setting internal.Setting
}
func NewAssetController() *AssetController {
@@ -33,7 +34,7 @@ func (r *AssetController) Index(ctx http.Context) http.Response {
}
// 拒绝访问非入口文件
if !strings.HasPrefix(ctx.Request().Path(), entrance) {
return ctx.Response().Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound))
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
path := strings.TrimPrefix(ctx.Request().Path(), entrance)
@@ -43,21 +44,21 @@ func (r *AssetController) Index(ctx http.Context) http.Response {
}
if !tools.Exists("public" + path) {
return ctx.Response().Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound))
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
file, err := os.Open("public" + path)
if err != nil {
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
return Error(ctx, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
stat, err := file.Stat()
if err != nil {
return ctx.Response().Status(http.StatusInternalServerError).String(http.StatusText(http.StatusInternalServerError))
return Error(ctx, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
}
if stat.IsDir() {
return ctx.Response().Status(http.StatusForbidden).String(http.StatusText(http.StatusForbidden))
return Error(ctx, http.StatusForbidden, http.StatusText(http.StatusForbidden))
}
return ctx.Response().Header("Cache-Control", "no-cache").File("public" + path)
@@ -72,5 +73,5 @@ func (r *AssetController) Robots(ctx http.Context) http.Response {
}
func (r *AssetController) NotFound(ctx http.Context) http.Response {
return ctx.Response().Status(http.StatusNotFound).String(http.StatusText(http.StatusNotFound))
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
+50 -20
View File
@@ -6,15 +6,15 @@ import (
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/internal"
"panel/internal/services"
"panel/pkg/acme"
)
type CertController struct {
cron services.Cron
cert services.Cert
cron internal.Cron
cert internal.Cert
}
func NewCertController() *CertController {
@@ -121,8 +121,8 @@ func (r *CertController) Algorithms(ctx http.Context) http.Response {
// @Tags 证书管理
// @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
@@ -141,9 +141,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,
})
}
@@ -273,8 +273,8 @@ func (r *CertController) UserDestroy(ctx http.Context) http.Response {
// @Tags 证书管理
// @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
@@ -293,9 +293,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,
})
}
@@ -425,8 +425,8 @@ func (r *CertController) DNSDestroy(ctx http.Context) http.Response {
// @Tags 证书管理
// @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
@@ -445,9 +445,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,
})
}
@@ -650,7 +650,7 @@ func (r *CertController) Renew(ctx http.Context) http.Response {
// @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
@@ -669,3 +669,33 @@ func (r *CertController) ManualDNS(ctx http.Context) http.Response {
return Success(ctx, resolves)
}
// Deploy
//
// @Summary 部署证书
// @Description 部署面板证书管理的证书
// @Tags 证书管理
// @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 := Sanitize(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
+5 -9
View File
@@ -2,25 +2,23 @@ package controllers
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
)
// 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,17 +26,15 @@ 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: facades.Lang(ctx).Get("messages.mistake") + ": " + 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"),
})
}
+8 -6
View File
@@ -8,14 +8,16 @@ import (
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/carbon"
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
)
type CronController struct {
cron services.Cron
setting services.Setting
cron internal.Cron
setting internal.Setting
}
func NewCronController() *CronController {
@@ -71,7 +73,7 @@ func (r *CronController) Add(ctx http.Context) http.Response {
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")
}
@@ -83,7 +85,7 @@ func (r *CronController) Add(ctx http.Context) http.Response {
shell = `#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子Linux面板 - 数据备份脚本
# 耗子 Linux 面板 - 数据备份脚本
type=` + backupType + `
path=` + backupPath + `
@@ -100,7 +102,7 @@ panel backup ${type} ${name} ${path} ${save} 2>&1
shell = `#!/bin/bash
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
# 耗子Linux面板 - 日志切割脚本
# 耗子 Linux 面板 - 日志切割脚本
name=` + website + `
save=` + cast.ToString(save) + `
+113 -42
View File
@@ -1,11 +1,16 @@
package controllers
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/support/carbon"
commonrequests "panel/app/http/requests/common"
requests "panel/app/http/requests/file"
"panel/pkg/tools"
@@ -47,13 +52,7 @@ func (r *FileController) Create(ctx http.Context) http.Response {
}
}
if err := tools.Chmod(request.Path, 0755); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.Chown(request.Path, "www", "www"); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Path, 0755, "www", "www")
return Success(ctx, nil)
}
@@ -75,6 +74,17 @@ func (r *FileController) Content(ctx http.Context) http.Response {
return sanitize
}
fileInfo, err := tools.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 := tools.Read(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
@@ -91,27 +101,26 @@ func (r *FileController) Content(ctx http.Context) http.Response {
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.Exist true "request"
// @Param data body requests.Save true "request"
// @Param content body string true "content"
// @Success 200 {object} SuccessResponse
// @Router /panel/file/save [post]
func (r *FileController) Save(ctx http.Context) http.Response {
var request requests.Exist
var request requests.Save
sanitize := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
content := ctx.Request().Input("content")
fileInfo, err := tools.FileInfo(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = tools.Write(request.Path, content, fileInfo.Mode()); err != nil {
if err = tools.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)
}
@@ -148,35 +157,37 @@ func (r *FileController) Delete(ctx http.Context) http.Response {
// @Accept json
// @Produce json
// @Security BearerToken
// @Param data body requests.NotExist true "request"
// @Param file formData file true "file"
// @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.NotExist
var request requests.Upload
sanitize := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
}
pathInfo, err := tools.FileInfo(request.Path)
src, err := request.File.Open()
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if !pathInfo.IsDir() {
return Error(ctx, http.StatusInternalServerError, "目标路径不是目录")
defer src.Close()
if tools.Exists(request.Path) && !ctx.Request().InputBool("force") {
return Error(ctx, http.StatusForbidden, "目标路径已存在,是否覆盖?")
}
file, err := ctx.Request().File("file")
if err != nil {
return Error(ctx, http.StatusInternalServerError, "上传文件失败")
}
_, err = file.Store(request.Path)
data, err := io.ReadAll(src)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err = tools.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)
}
@@ -198,14 +209,15 @@ func (r *FileController) Move(ctx http.Context) http.Response {
return sanitize
}
if tools.Exists(request.New) && !ctx.Request().InputBool("force") {
return Error(ctx, http.StatusInternalServerError, "目标路径已存在,是否覆盖?")
if tools.Exists(request.Target) && !ctx.Request().InputBool("force") {
return Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在")
}
if err := tools.Mv(request.Old, request.New); err != nil {
if err := tools.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)
}
@@ -227,14 +239,15 @@ func (r *FileController) Copy(ctx http.Context) http.Response {
return sanitize
}
if tools.Exists(request.New) && !ctx.Request().InputBool("force") {
return Error(ctx, http.StatusInternalServerError, "目标路径已存在,是否覆盖?")
if tools.Exists(request.Target) && !ctx.Request().InputBool("force") {
return Error(ctx, http.StatusForbidden, "目标路径"+request.Target+"已存在")
}
if err := tools.Cp(request.Old, request.New); err != nil {
if err := tools.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)
}
@@ -250,7 +263,7 @@ func (r *FileController) Copy(ctx http.Context) http.Response {
// @Success 200 {object} SuccessResponse
// @Router /panel/file/download [get]
func (r *FileController) Download(ctx http.Context) http.Response {
var request requests.NotExist
var request requests.Exist
sanitize := Sanitize(ctx, &request)
if sanitize != nil {
return sanitize
@@ -312,7 +325,14 @@ func (r *FileController) Info(ctx http.Context) http.Response {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, fileInfo)
return Success(ctx, http.Json{
"name": fileInfo.Name(),
"size": tools.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
@@ -336,6 +356,9 @@ func (r *FileController) Permission(ctx http.Context) http.Response {
if err := tools.Chmod(request.Path, os.FileMode(request.Mode)); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
if err := tools.Chown(request.Path, request.Owner, request.Group); err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, nil)
}
@@ -362,6 +385,7 @@ func (r *FileController) Archive(ctx http.Context) http.Response {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.File, 0755, "www", "www")
return Success(ctx, nil)
}
@@ -387,6 +411,7 @@ func (r *FileController) UnArchive(ctx http.Context) http.Response {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
r.setPermission(request.Path, 0755, "www", "www")
return Success(ctx, nil)
}
@@ -431,7 +456,8 @@ func (r *FileController) Search(ctx http.Context) http.Response {
// @Tags 文件管理
// @Accept json
// @Produce json
// @Param data query requests.Exist true "request"
// @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 {
@@ -441,17 +467,62 @@ func (r *FileController) List(ctx http.Context) http.Response {
return sanitize
}
paths := make(map[string]os.FileInfo)
err := filepath.Walk(request.Path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
paths[path] = info
return nil
})
var paginate commonrequests.Paginate
paginateSanitize := Sanitize(ctx, &paginate)
if paginateSanitize != nil {
return paginateSanitize
}
fileInfoList, err := os.ReadDir(request.Path)
if err != nil {
return Error(ctx, http.StatusInternalServerError, err.Error())
}
return Success(ctx, paths)
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": tools.FormatBytes(float64(info.Size())),
"mode_str": info.Mode().String(),
"mode": fmt.Sprintf("%04o", info.Mode().Perm()),
"owner": tools.GetUser(stat.Uid),
"group": tools.GetGroup(stat.Gid),
"uid": stat.Uid,
"gid": stat.Gid,
"hidden": tools.IsHidden(info.Name()),
"symlink": tools.IsSymlink(info.Mode()),
"link": tools.GetSymlink(filepath.Join(request.Path, info.Name())),
"dir": info.IsDir(),
"modify": carbon.FromStdTime(info.ModTime()).ToDateTimeString(),
})
}
start := paginate.Limit * (paginate.Page - 1)
end := paginate.Limit * paginate.Page
if start > len(paths) {
start = len(paths)
}
if end > len(paths) {
end = len(paths)
}
paged := paths[start:end]
if paged == nil {
paged = []any{}
}
return Success(ctx, http.Json{
"total": len(paths),
"items": paged,
})
}
// setPermission
func (r *FileController) setPermission(path string, mode uint, owner, group string) {
_ = tools.Chmod(path, os.FileMode(mode))
_ = tools.Chown(path, owner, group)
}
+9 -6
View File
@@ -10,7 +10,8 @@ import (
"github.com/goravel/framework/facades"
"panel/app/models"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
)
@@ -22,8 +23,8 @@ type MenuItem struct {
}
type InfoController struct {
plugin services.Plugin
setting services.Setting
plugin internal.Plugin
setting internal.Setting
}
func NewInfoController() *InfoController {
@@ -60,7 +61,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,
})
}
@@ -308,14 +309,16 @@ func (r *InfoController) Update(ctx http.Context) http.Response {
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
}
err = tools.UpdatePanel(panel)
if err != nil {
internal.Status = internal.StatusUpgrade
if err = tools.UpdatePanel(panel); err != nil {
internal.Status = internal.StatusFailed
facades.Log().Request(ctx.Request()).Tags("面板", "基础信息").With(map[string]any{
"error": err.Error(),
}).Info("更新面板失败")
return Error(ctx, http.StatusInternalServerError, err.Error())
}
internal.Status = internal.StatusNormal
tools.RestartPanel()
return Success(ctx, nil)
}
+3 -2
View File
@@ -9,11 +9,12 @@ import (
"github.com/spf13/cast"
"panel/app/models"
"panel/app/services"
"panel/internal"
"panel/internal/services"
)
type MonitorController struct {
setting services.Setting
setting internal.Setting
}
func NewMonitorController() *MonitorController {
+10 -164
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"
"panel/internal"
"panel/internal/services"
)
type PluginController struct {
plugin services.Plugin
task services.Task
plugin internal.Plugin
task internal.Task
}
func NewPluginController() *PluginController {
@@ -30,13 +29,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 {
@@ -96,183 +92,33 @@ func (r *PluginController) List(ctx http.Context) http.Response {
// Install 安装插件
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("检查插件安装状态失败")
if err := r.plugin.Install(slug); err != nil {
return ErrorSystem(ctx)
}
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 卸载插件
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("检查插件安装状态失败")
if err := r.plugin.Uninstall(slug); err != nil {
return ErrorSystem(ctx)
}
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 更新插件
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("检查插件安装状态失败")
if err := r.plugin.Update(slug); err != nil {
return ErrorSystem(ctx)
}
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, "任务已提交")
}
-39
View File
@@ -1,39 +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"`
}
type RsyncModule struct {
Name string `json:"name"`
Path string `json:"path"`
Comment string `json:"comment"`
ReadOnly bool `json:"read_only"`
AuthUser string `json:"auth_user"`
Secret string `json:"secret"`
HostsAllow string `json:"hosts_allow"`
}
@@ -10,12 +10,14 @@ import (
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
"panel/types"
)
type Fail2banController struct {
website services.Website
website internal.Website
}
func NewFail2banController() *Fail2banController {
@@ -88,7 +90,7 @@ 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
@@ -105,7 +107,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],
@@ -120,7 +122,7 @@ func (r *Fail2banController) List(ctx http.Context) http.Response {
if startIndex > len(jails) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []Fail2banJail{},
"items": []types.Fail2banJail{},
})
}
if endIndex > len(jails) {
@@ -128,7 +130,7 @@ func (r *Fail2banController) List(ctx http.Context) http.Response {
}
pagedJails := jails[startIndex:endIndex]
if pagedJails == nil {
pagedJails = []Fail2banJail{}
pagedJails = []types.Fail2banJail{}
}
return controllers.Success(ctx, http.Json{
@@ -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)
}
@@ -10,24 +10,25 @@ import (
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
)
type Mysql80Controller struct {
setting services.Setting
backup services.Backup
type MySQLController struct {
setting internal.Setting
backup internal.Backup
}
func NewMysql80Controller() *Mysql80Controller {
return &Mysql80Controller{
func NewMySQLController() *MySQLController {
return &MySQLController{
setting: services.NewSettingImpl(),
backup: services.NewBackupImpl(),
}
}
// Status 获取运行状态
func (r *Mysql80Controller) Status(ctx http.Context) http.Response {
func (r *MySQLController) Status(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
@@ -37,7 +38,7 @@ func (r *Mysql80Controller) Status(ctx http.Context) http.Response {
}
// Reload 重载配置
func (r *Mysql80Controller) Reload(ctx http.Context) http.Response {
func (r *MySQLController) Reload(ctx http.Context) http.Response {
if err := tools.ServiceReload("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重载MySQL配置失败")
}
@@ -46,7 +47,7 @@ func (r *Mysql80Controller) Reload(ctx http.Context) http.Response {
}
// Restart 重启服务
func (r *Mysql80Controller) Restart(ctx http.Context) http.Response {
func (r *MySQLController) Restart(ctx http.Context) http.Response {
if err := tools.ServiceRestart("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "重启MySQL服务失败")
}
@@ -55,7 +56,7 @@ func (r *Mysql80Controller) Restart(ctx http.Context) http.Response {
}
// Start 启动服务
func (r *Mysql80Controller) Start(ctx http.Context) http.Response {
func (r *MySQLController) Start(ctx http.Context) http.Response {
if err := tools.ServiceStart("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "启动MySQL服务失败")
}
@@ -64,7 +65,7 @@ func (r *Mysql80Controller) Start(ctx http.Context) http.Response {
}
// Stop 停止服务
func (r *Mysql80Controller) Stop(ctx http.Context) http.Response {
func (r *MySQLController) Stop(ctx http.Context) http.Response {
if err := tools.ServiceStop("mysqld"); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "停止MySQL服务失败")
}
@@ -73,7 +74,7 @@ func (r *Mysql80Controller) Stop(ctx http.Context) http.Response {
}
// GetConfig 获取配置
func (r *Mysql80Controller) GetConfig(ctx http.Context) http.Response {
func (r *MySQLController) 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配置失败")
@@ -83,7 +84,7 @@ func (r *Mysql80Controller) GetConfig(ctx http.Context) http.Response {
}
// SaveConfig 保存配置
func (r *Mysql80Controller) SaveConfig(ctx http.Context) http.Response {
func (r *MySQLController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if len(config) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "配置不能为空")
@@ -97,7 +98,7 @@ func (r *Mysql80Controller) SaveConfig(ctx http.Context) http.Response {
}
// Load 获取负载
func (r *Mysql80Controller) Load(ctx http.Context) http.Response {
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密码为空")
@@ -167,7 +168,7 @@ func (r *Mysql80Controller) Load(ctx http.Context) http.Response {
}
// ErrorLog 获取错误日志
func (r *Mysql80Controller) ErrorLog(ctx http.Context) http.Response {
func (r *MySQLController) 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)
@@ -177,7 +178,7 @@ func (r *Mysql80Controller) ErrorLog(ctx http.Context) http.Response {
}
// ClearErrorLog 清空错误日志
func (r *Mysql80Controller) ClearErrorLog(ctx http.Context) http.Response {
func (r *MySQLController) 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)
}
@@ -186,7 +187,7 @@ func (r *Mysql80Controller) ClearErrorLog(ctx http.Context) http.Response {
}
// SlowLog 获取慢查询日志
func (r *Mysql80Controller) SlowLog(ctx http.Context) http.Response {
func (r *MySQLController) 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)
@@ -196,7 +197,7 @@ func (r *Mysql80Controller) SlowLog(ctx http.Context) http.Response {
}
// ClearSlowLog 清空慢查询日志
func (r *Mysql80Controller) ClearSlowLog(ctx http.Context) http.Response {
func (r *MySQLController) 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)
}
@@ -204,7 +205,7 @@ func (r *Mysql80Controller) ClearSlowLog(ctx http.Context) http.Response {
}
// GetRootPassword 获取root密码
func (r *Mysql80Controller) GetRootPassword(ctx http.Context) http.Response {
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密码为空")
@@ -214,7 +215,7 @@ func (r *Mysql80Controller) GetRootPassword(ctx http.Context) http.Response {
}
// SetRootPassword 设置root密码
func (r *Mysql80Controller) SetRootPassword(ctx http.Context) http.Response {
func (r *MySQLController) SetRootPassword(ctx http.Context) http.Response {
status, err := tools.ServiceStatus("mysqld")
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
@@ -252,7 +253,7 @@ func (r *Mysql80Controller) SetRootPassword(ctx http.Context) http.Response {
}
// DatabaseList 获取数据库列表
func (r *Mysql80Controller) DatabaseList(ctx http.Context) http.Response {
func (r *MySQLController) DatabaseList(ctx http.Context) http.Response {
rootPassword := r.setting.Get(models.SettingKeyMysqlRootPassword)
type database struct {
Name string `json:"name"`
@@ -307,7 +308,7 @@ func (r *Mysql80Controller) DatabaseList(ctx http.Context) http.Response {
}
// AddDatabase 添加数据库
func (r *Mysql80Controller) AddDatabase(ctx http.Context) http.Response {
func (r *MySQLController) 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_]+$",
@@ -342,7 +343,7 @@ func (r *Mysql80Controller) AddDatabase(ctx http.Context) http.Response {
}
// DeleteDatabase 删除数据库
func (r *Mysql80Controller) DeleteDatabase(ctx http.Context) http.Response {
func (r *MySQLController) 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",
})
@@ -363,7 +364,7 @@ func (r *Mysql80Controller) DeleteDatabase(ctx http.Context) http.Response {
}
// BackupList 获取备份列表
func (r *Mysql80Controller) BackupList(ctx http.Context) http.Response {
func (r *MySQLController) BackupList(ctx http.Context) http.Response {
backupList, err := r.backup.MysqlList()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
@@ -376,7 +377,7 @@ func (r *Mysql80Controller) BackupList(ctx http.Context) http.Response {
if startIndex > len(backupList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []services.BackupFile{},
"items": []internal.BackupFile{},
})
}
if endIndex > len(backupList) {
@@ -384,7 +385,7 @@ func (r *Mysql80Controller) BackupList(ctx http.Context) http.Response {
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []services.BackupFile{}
pagedBackupList = []internal.BackupFile{}
}
return controllers.Success(ctx, http.Json{
@@ -394,7 +395,7 @@ func (r *Mysql80Controller) BackupList(ctx http.Context) http.Response {
}
// UploadBackup 上传备份
func (r *Mysql80Controller) UploadBackup(ctx http.Context) http.Response {
func (r *MySQLController) UploadBackup(ctx http.Context) http.Response {
file, err := ctx.Request().File("file")
if err != nil {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "上传文件失败")
@@ -417,7 +418,7 @@ func (r *Mysql80Controller) UploadBackup(ctx http.Context) http.Response {
}
// CreateBackup 创建备份
func (r *Mysql80Controller) CreateBackup(ctx http.Context) http.Response {
func (r *MySQLController) 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",
})
@@ -438,7 +439,7 @@ func (r *Mysql80Controller) CreateBackup(ctx http.Context) http.Response {
}
// DeleteBackup 删除备份
func (r *Mysql80Controller) DeleteBackup(ctx http.Context) http.Response {
func (r *MySQLController) DeleteBackup(ctx http.Context) http.Response {
validator, err := ctx.Request().Validate(map[string]string{
"name": "required|min_len:1|max_len:255",
})
@@ -459,7 +460,7 @@ func (r *Mysql80Controller) DeleteBackup(ctx http.Context) http.Response {
}
// RestoreBackup 还原备份
func (r *Mysql80Controller) RestoreBackup(ctx http.Context) http.Response {
func (r *MySQLController) 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",
@@ -480,7 +481,7 @@ func (r *Mysql80Controller) RestoreBackup(ctx http.Context) http.Response {
}
// UserList 用户列表
func (r *Mysql80Controller) UserList(ctx http.Context) http.Response {
func (r *MySQLController) UserList(ctx http.Context) http.Response {
type user struct {
User string `json:"user"`
Host string `json:"host"`
@@ -559,7 +560,7 @@ func (r *Mysql80Controller) UserList(ctx http.Context) http.Response {
}
// AddUser 添加用户
func (r *Mysql80Controller) AddUser(ctx http.Context) http.Response {
func (r *MySQLController) 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_]+$",
@@ -590,7 +591,7 @@ func (r *Mysql80Controller) AddUser(ctx http.Context) http.Response {
}
// DeleteUser 删除用户
func (r *Mysql80Controller) DeleteUser(ctx http.Context) http.Response {
func (r *MySQLController) 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_]+$",
})
@@ -611,7 +612,7 @@ func (r *Mysql80Controller) DeleteUser(ctx http.Context) http.Response {
}
// SetUserPassword 设置用户密码
func (r *Mysql80Controller) SetUserPassword(ctx http.Context) http.Response {
func (r *MySQLController) 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",
@@ -637,7 +638,7 @@ func (r *Mysql80Controller) SetUserPassword(ctx http.Context) http.Response {
}
// SetUserPrivileges 设置用户权限
func (r *Mysql80Controller) SetUserPrivileges(ctx http.Context) http.Response {
func (r *MySQLController) 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",
@@ -4,8 +4,8 @@ import (
"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"
@@ -117,9 +117,9 @@ func (r *OpenRestyController) ClearErrorLog(ctx http.Context) http.Response {
// Load 获取负载
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() {
if err != nil || !resp.IsSuccess() {
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty负载失败")
}
@@ -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
}
@@ -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 Php83Controller struct {
setting services.Setting
task services.Task
version string
}
func NewPhp83Controller() *Php83Controller {
return &Php83Controller{
setting: services.NewSettingImpl(),
task: services.NewTaskImpl(),
version: "83",
}
}
func (r *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) GetExtensionList(ctx http.Context) http.Response {
extensions := r.GetExtensions()
return controllers.Success(ctx, extensions)
}
func (r *Php83Controller) 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 *Php83Controller) 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 *Php83Controller) 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,167 @@
package plugins
import (
"github.com/goravel/framework/contracts/http"
"panel/app/http/controllers"
"panel/internal"
"panel/internal/services"
)
type PHPController struct {
service internal.PHP
}
func NewPHPController(version uint) *PHPController {
return &PHPController{
service: services.NewPHPImpl(version),
}
}
func (r *PHPController) Status(ctx http.Context) http.Response {
status, err := r.service.Status()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, status)
}
func (r *PHPController) Reload(ctx http.Context) http.Response {
if err := r.service.Reload(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) Start(ctx http.Context) http.Response {
if err := r.service.Start(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) Stop(ctx http.Context) http.Response {
if err := r.service.Stop(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) Restart(ctx http.Context) http.Response {
if err := r.service.Restart(); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) GetConfig(ctx http.Context) http.Response {
config, err := r.service.GetConfig()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
func (r *PHPController) SaveConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if err := r.service.SaveConfig(config); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) GetFPMConfig(ctx http.Context) http.Response {
config, err := r.service.GetFPMConfig()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, config)
}
func (r *PHPController) SaveFPMConfig(ctx http.Context) http.Response {
config := ctx.Request().Input("config")
if err := r.service.SaveFPMConfig(config); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) Load(ctx http.Context) http.Response {
load, err := r.service.Load()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, load)
}
func (r *PHPController) ErrorLog(ctx http.Context) http.Response {
log, _ := r.service.GetErrorLog()
return controllers.Success(ctx, log)
}
func (r *PHPController) SlowLog(ctx http.Context) http.Response {
log, _ := r.service.GetSlowLog()
return controllers.Success(ctx, log)
}
func (r *PHPController) ClearErrorLog(ctx http.Context) http.Response {
err := r.service.ClearErrorLog()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) ClearSlowLog(ctx http.Context) http.Response {
err := r.service.ClearSlowLog()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) GetExtensionList(ctx http.Context) http.Response {
extensions, err := r.service.GetExtensions()
if err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, extensions)
}
func (r *PHPController) InstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
if err := r.service.InstallExtension(slug); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
func (r *PHPController) UninstallExtension(ctx http.Context) http.Response {
slug := ctx.Request().Input("slug")
if len(slug) == 0 {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "参数错误")
}
if err := r.service.UninstallExtension(slug); err != nil {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
return controllers.Success(ctx, nil)
}
@@ -8,13 +8,15 @@ import (
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
"panel/types"
)
type Postgresql15Controller struct {
setting services.Setting
backup services.Backup
setting internal.Setting
backup internal.Backup
}
func NewPostgresql15Controller() *Postgresql15Controller {
@@ -151,12 +153,12 @@ func (r *Postgresql15Controller) Load(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL空间占用失败")
}
data := []LoadInfo{
{"启动时间", carbon.Parse(time).ToDateTimeString()},
{"进程 PID", pid},
{"进程数", process},
{"总连接数", connections},
{"空间占用", storage},
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)
@@ -319,7 +321,7 @@ func (r *Postgresql15Controller) BackupList(ctx http.Context) http.Response {
if startIndex > len(backupList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []services.BackupFile{},
"items": []internal.BackupFile{},
})
}
if endIndex > len(backupList) {
@@ -327,7 +329,7 @@ func (r *Postgresql15Controller) BackupList(ctx http.Context) http.Response {
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []services.BackupFile{}
pagedBackupList = []internal.BackupFile{}
}
return controllers.Success(ctx, http.Json{
@@ -8,13 +8,15 @@ import (
"panel/app/http/controllers"
"panel/app/models"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
"panel/types"
)
type Postgresql16Controller struct {
setting services.Setting
backup services.Backup
setting internal.Setting
backup internal.Backup
}
func NewPostgresql16Controller() *Postgresql16Controller {
@@ -151,12 +153,12 @@ func (r *Postgresql16Controller) Load(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL空间占用失败")
}
data := []LoadInfo{
{"启动时间", carbon.Parse(time).ToDateTimeString()},
{"进程 PID", pid},
{"进程数", process},
{"总连接数", connections},
{"空间占用", storage},
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)
@@ -319,7 +321,7 @@ func (r *Postgresql16Controller) BackupList(ctx http.Context) http.Response {
if startIndex > len(backupList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []services.BackupFile{},
"items": []internal.BackupFile{},
})
}
if endIndex > len(backupList) {
@@ -327,7 +329,7 @@ func (r *Postgresql16Controller) BackupList(ctx http.Context) http.Response {
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []services.BackupFile{}
pagedBackupList = []internal.BackupFile{}
}
return controllers.Success(ctx, http.Json{
@@ -7,6 +7,7 @@ import (
"panel/app/http/controllers"
"panel/pkg/tools"
"panel/types"
)
type RedisController struct {
@@ -103,20 +104,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)
@@ -9,12 +9,14 @@ import (
"panel/app/http/controllers"
commonrequests "panel/app/http/requests/common"
requests "panel/app/http/requests/plugins/rsync"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
"panel/types"
)
type RsyncController struct {
setting services.Setting
setting internal.Setting
}
func NewRsyncController() *RsyncController {
@@ -103,7 +105,7 @@ func (r *RsyncController) Stop(ctx http.Context) http.Response {
// @Tags 插件-Rsync
// @Produce json
// @Security BearerToken
// @Param data body commonrequests.Paginate true "request"
// @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 {
@@ -118,9 +120,9 @@ func (r *RsyncController) List(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusInternalServerError, err.Error())
}
var modules []RsyncModule
var modules []types.RsyncModule
lines := strings.Split(config, "\n")
var currentModule *RsyncModule
var currentModule *types.RsyncModule
for _, line := range lines {
line = strings.TrimSpace(line)
@@ -134,7 +136,7 @@ func (r *RsyncController) List(ctx http.Context) http.Response {
modules = append(modules, *currentModule)
}
moduleName := line[1 : len(line)-1]
currentModule = &RsyncModule{
currentModule = &types.RsyncModule{
Name: moduleName,
}
} else if currentModule != nil {
@@ -172,7 +174,7 @@ func (r *RsyncController) List(ctx http.Context) http.Response {
if startIndex > len(modules) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []RsyncModule{},
"items": []types.RsyncModule{},
})
}
if endIndex > len(modules) {
@@ -180,7 +182,7 @@ func (r *RsyncController) List(ctx http.Context) http.Response {
}
pagedModules := modules[startIndex:endIndex]
if pagedModules == nil {
pagedModules = []RsyncModule{}
pagedModules = []types.RsyncModule{}
}
return controllers.Success(ctx, http.Json{
+12 -10
View File
@@ -9,12 +9,14 @@ import (
"github.com/spf13/cast"
"panel/app/http/controllers"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
"panel/types"
)
type S3fsController struct {
setting services.Setting
setting internal.Setting
}
func NewS3fsController() *S3fsController {
@@ -28,7 +30,7 @@ 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 挂载失败")
@@ -39,7 +41,7 @@ func (r *S3fsController) List(ctx http.Context) http.Response {
if startIndex > len(s3fsList) {
return controllers.Success(ctx, http.Json{
"total": 0,
"items": []S3fsMount{},
"items": []types.S3fsMount{},
})
}
if endIndex > len(s3fsList) {
@@ -47,7 +49,7 @@ func (r *S3fsController) List(ctx http.Context) http.Response {
}
pagedS3fsList := s3fsList[startIndex:endIndex]
if pagedS3fsList == nil {
pagedS3fsList = []S3fsMount{}
pagedS3fsList = []types.S3fsMount{}
}
return controllers.Success(ctx, http.Json{
@@ -93,7 +95,7 @@ func (r *S3fsController) Add(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空")
}
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 挂载失败")
@@ -123,7 +125,7 @@ func (r *S3fsController) Add(ctx http.Context) http.Response {
return controllers.Error(ctx, http.StatusInternalServerError, "挂载失败,请检查配置是否正确")
}
s3fsList = append(s3fsList, S3fsMount{
s3fsList = append(s3fsList, types.S3fsMount{
ID: id,
Path: path,
Bucket: bucket,
@@ -148,13 +150,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
@@ -181,7 +183,7 @@ func (r *S3fsController) Delete(ctx http.Context) http.Response {
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)
+29 -17
View File
@@ -6,14 +6,14 @@ import (
"github.com/spf13/cast"
requests "panel/app/http/requests/setting"
responses "panel/app/http/responses/setting"
"panel/app/models"
"panel/app/services"
"panel/internal"
"panel/internal/services"
"panel/pkg/tools"
)
type SettingController struct {
setting services.Setting
setting internal.Setting
}
func NewSettingController() *SettingController {
@@ -29,7 +29,7 @@ func NewSettingController() *SettingController {
// @Tags 面板设置
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse{data=responses.Settings}
// @Success 200 {object} SuccessResponse
// @Router /panel/setting/list [get]
func (r *SettingController) List(ctx http.Context) http.Response {
var settings []models.Setting
@@ -41,24 +41,16 @@ 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)
var user models.User
err = facades.Auth().User(ctx, &user)
err = facades.Auth(ctx).User(&user)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "面板设置").With(map[string]any{
"error": err.Error(),
}).Info("获取用户信息失败")
return ErrorSystem(ctx)
}
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 := tools.Exec(`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,7 +58,17 @@ 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),
"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
@@ -124,7 +126,7 @@ func (r *SettingController) Update(ctx http.Context) http.Response {
}
var user models.User
err = facades.Auth().User(ctx, &user)
err = facades.Auth(ctx).User(&user)
if err != nil {
return ErrorSystem(ctx)
}
@@ -195,7 +197,17 @@ func (r *SettingController) Update(ctx http.Context) http.Response {
}
}
if oldPort != port || oldEntrance != entrance {
if updateRequest.SSL {
if out, err := tools.Exec("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 := tools.Exec("sed -i 's/APP_SSL=true/APP_SSL=false/g' /www/panel/panel.conf"); err != nil {
return Error(ctx, http.StatusInternalServerError, out)
}
}
if oldPort != port || oldEntrance != entrance || updateRequest.SSL != facades.Config().GetBool("panel.ssl") {
tools.RestartPanel()
}
+58 -57
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/internal"
"panel/internal/services"
"panel/pkg/ssh"
)
type SshController struct {
AuthMethod ssh.AuthMethod
setting services.Setting
setting internal.Setting
}
func NewSshController() *SshController {
@@ -85,15 +85,24 @@ 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")},
}
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.SSHClientConfigPassword(
r.setting.Get(models.SettingKeySshHost)+":"+r.setting.Get(models.SettingKeySshPort),
r.setting.Get(models.SettingKeySshUser),
@@ -101,59 +110,51 @@ func (r *SshController) Session(ctx http.Context) http.Response {
)
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
}
+10 -163
View File
@@ -1,35 +1,19 @@
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/goravel/framework/facades"
"github.com/swaggo/http-swagger/v2"
_ "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 +25,12 @@ 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,
if !facades.Config().GetBool("app.debug") {
return Error(ctx, http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
// create a template with name
index, _ := template.New("swagger_index.html").Parse(indexTemplate)
handler := httpSwagger.Handler()
handler(ctx.Response().Writer(), ctx.Request().Origin())
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>
`
+1 -1
View File
@@ -63,7 +63,7 @@ func (r *TaskController) Log(ctx http.Context) http.Response {
return ErrorSystem(ctx)
}
log, err := tools.Exec("tail -n 1000 " + task.Log + " | tac")
log, err := tools.Exec(`tail -n 500 '` + task.Log + `'`)
if err != nil {
return Error(ctx, http.StatusInternalServerError, "日志已被清理")
}
+8 -9
View File
@@ -5,7 +5,6 @@ import (
"github.com/goravel/framework/facades"
"panel/app/http/requests/user"
responses "panel/app/http/responses/user"
"panel/app/models"
)
@@ -61,7 +60,7 @@ func (r *UserController) Login(ctx http.Context) http.Response {
}
}
token, loginErr := facades.Auth().LoginUsingID(ctx, user.ID)
token, loginErr := facades.Auth(ctx).LoginUsingID(user.ID)
if loginErr != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
"error": err.Error(),
@@ -81,11 +80,11 @@ func (r *UserController) Login(ctx http.Context) http.Response {
// @Tags 用户鉴权
// @Produce json
// @Security BearerToken
// @Success 200 {object} SuccessResponse{data=responses.Info}
// @Success 200 {object} SuccessResponse
// @Router /panel/user/info [get]
func (r *UserController) Info(ctx http.Context) http.Response {
var user models.User
err := facades.Auth().User(ctx, &user)
err := facades.Auth(ctx).User(&user)
if err != nil {
facades.Log().Request(ctx.Request()).Tags("面板", "用户").With(map[string]any{
"error": err.Error(),
@@ -93,10 +92,10 @@ func (r *UserController) Info(ctx http.Context) http.Response {
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,
})
}
+17 -17
View File
@@ -10,16 +10,16 @@ import (
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/internal"
"panel/internal/services"
"panel/pkg/tools"
)
type WebsiteController struct {
website services.Website
setting services.Setting
backup services.Backup
website internal.Website
setting internal.Setting
backup internal.Backup
}
func NewWebsiteController() *WebsiteController {
@@ -37,8 +37,8 @@ func NewWebsiteController() *WebsiteController {
// @Tags 网站管理
// @Produce json
// @Security BearerToken
// @Param data body commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse{data=responses.List}
// @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
@@ -55,9 +55,9 @@ 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,
})
}
@@ -83,7 +83,7 @@ func (r *WebsiteController) Add(ctx http.Context) http.Response {
addRequest.Path = r.setting.Get(models.SettingKeyWebsitePath) + "/" + addRequest.Name
}
website := services.PanelWebsite{
website := internal.PanelWebsite{
Name: addRequest.Name,
Status: true,
Domains: addRequest.Domains,
@@ -205,7 +205,7 @@ func (r *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response {
// @Produce json
// @Security BearerToken
// @Param id path int true "网站 ID"
// @Success 200 {object} SuccessResponse{data=services.PanelWebsite}
// @Success 200 {object} SuccessResponse{data=internal.PanelWebsite}
// @Router /panel/websites/{id}/config [get]
func (r *WebsiteController) GetConfig(ctx http.Context) http.Response {
var idRequest requests.ID
@@ -327,8 +327,8 @@ func (r *WebsiteController) UpdateRemark(ctx http.Context) http.Response {
// @Tags 网站管理
// @Produce json
// @Security BearerToken
// @Param data body commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse{data=[]services.BackupFile}
// @Param data query commonrequests.Paginate true "request"
// @Success 200 {object} SuccessResponse{data=[]internal.BackupFile}
// @Router /panel/website/backupList [get]
func (r *WebsiteController) BackupList(ctx http.Context) http.Response {
var paginateRequest commonrequests.Paginate
@@ -350,7 +350,7 @@ func (r *WebsiteController) BackupList(ctx http.Context) http.Response {
if startIndex > len(backupList) {
return Success(ctx, http.Json{
"total": 0,
"items": []services.BackupFile{},
"items": []internal.BackupFile{},
})
}
if endIndex > len(backupList) {
@@ -358,7 +358,7 @@ func (r *WebsiteController) BackupList(ctx http.Context) http.Response {
}
pagedBackupList := backupList[startIndex:endIndex]
if pagedBackupList == nil {
pagedBackupList = []services.BackupFile{}
pagedBackupList = []internal.BackupFile{}
}
return Success(ctx, http.Json{
@@ -567,7 +567,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;
+5 -1
View File
@@ -2,6 +2,8 @@ package http
import (
"github.com/goravel/framework/contracts/http"
"panel/app/http/middleware"
)
type Kernel struct {
@@ -10,5 +12,7 @@ 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{
middleware.Status(),
}
}
+9 -11
View File
@@ -11,33 +11,31 @@ import (
// Jwt 确保通过 JWT 鉴权
func Jwt() http.Middleware {
return func(ctx http.Context) {
translate := facades.Lang(ctx)
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": "未登录",
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.token.missing"),
})
return
}
// JWT 鉴权
if _, err := facades.Auth().Parse(ctx, token); err != nil {
if _, err := facades.Auth(ctx).Parse(token); err != nil {
if errors.Is(err, auth.ErrorTokenExpired) {
token, err = facades.Auth().Refresh(ctx)
token, err = facades.Auth(ctx).Refresh()
if err != nil {
// 到达刷新时间上限
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": 401,
"message": "登录已过期",
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.token.expired"),
})
return
}
token = "Bearer " + token
} else {
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
"code": 401,
"message": "登录已过期",
ctx.Request().AbortWithStatusJson(http.StatusUnauthorized, http.Json{
"message": translate.Get("auth.token.expired"),
})
return
}
+29 -25
View File
@@ -2,26 +2,27 @@ 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"
"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 {
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 +33,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
}
+40
View File
@@ -0,0 +1,40 @@
package middleware
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"panel/internal"
)
// Status 检查程序状态
func Status() http.Middleware {
return func(ctx http.Context) {
translate := facades.Lang(ctx)
switch internal.Status {
case internal.StatusUpgrade:
ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{
"message": translate.Get("status.upgrade"),
})
return
case internal.StatusMaintain:
ctx.Request().AbortWithStatusJson(http.StatusServiceUnavailable, http.Json{
"message": translate.Get("status.maintain"),
})
return
case internal.StatusClosed:
ctx.Request().AbortWithStatusJson(http.StatusForbidden, http.Json{
"message": translate.Get("status.closed"),
})
return
case internal.StatusFailed:
ctx.Request().AbortWithStatusJson(http.StatusInternalServerError, http.Json{
"message": translate.Get("status.failed"),
})
return
default:
ctx.Request().Next()
return
}
}
}
+2 -2
View File
@@ -6,8 +6,8 @@ import (
)
type CertDeploy struct {
ID uint `form:"id" json:"id"`
WebsiteID uint `form:"website_id" json:"website_id"`
ID uint `form:"id" json:"id" filter:"uint"`
WebsiteID uint `form:"website_id" json:"website_id" filter:"uint"`
}
func (r *CertDeploy) Authorize(ctx http.Context) error {
-1
View File
@@ -26,7 +26,6 @@ func (r *DNSStore) Rules(ctx http.Context) map[string]string {
"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.api_key": "required_if:type,cloudflare",
}
}
@@ -0,0 +1,77 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"panel/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" filter:"int"`
CPUs int64 `form:"cpus" json:"cpus" filter:"int"`
Memory int64 `form:"memory" json:"memory" filter:"int"`
}
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) 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,34 @@
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) 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,70 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"panel/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) 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
}
+32
View File
@@ -0,0 +1,32 @@
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) 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
}
+38
View File
@@ -0,0 +1,38 @@
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) 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,34 @@
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) 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,44 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"panel/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) 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,40 @@
package requests
import (
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
"panel/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) 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
}
+2 -2
View File
@@ -17,8 +17,8 @@ func (r *Archive) Authorize(ctx http.Context) error {
func (r *Archive) Rules(ctx http.Context) map[string]string {
return map[string]string{
"paths": "array",
"paths.*": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$|path_exists",
"file": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$|path_not_exists",
"paths.*": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"file": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_not_exists`,
}
}
+4 -4
View File
@@ -6,8 +6,8 @@ import (
)
type Copy struct {
Old string `form:"old" json:"old"`
New string `form:"new" json:"new"`
Source string `form:"source" json:"source"`
Target string `form:"target" json:"target"`
}
func (r *Copy) Authorize(ctx http.Context) error {
@@ -16,8 +16,8 @@ func (r *Copy) Authorize(ctx http.Context) error {
func (r *Copy) Rules(ctx http.Context) map[string]string {
return map[string]string{
"old": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$|path_exists",
"new": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$",
"source": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"target": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$`,
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ func (r *Exist) Authorize(ctx http.Context) error {
func (r *Exist) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$|path_exists",
"path": `regex:^/([a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*)?$|path_exists`,
}
}
+4 -4
View File
@@ -6,8 +6,8 @@ import (
)
type Move struct {
Old string `form:"old" json:"old"`
New string `form:"new" json:"new"`
Source string `form:"source" json:"source"`
Target string `form:"target" json:"target"`
}
func (r *Move) Authorize(ctx http.Context) error {
@@ -16,8 +16,8 @@ func (r *Move) Authorize(ctx http.Context) error {
func (r *Move) Rules(ctx http.Context) map[string]string {
return map[string]string{
"old": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$|path_exists",
"new": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$|path_not_exists",
"source": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"target": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$`,
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ func (r *NotExist) Authorize(ctx http.Context) error {
func (r *NotExist) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$|path_not_exists",
"path": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_not_exists`,
}
}
+1 -1
View File
@@ -18,7 +18,7 @@ func (r *Permission) Authorize(ctx http.Context) error {
func (r *Permission) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$|path_exists",
"path": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"mode": "regex:^[0-7]{3}$|uint",
"owner": "regex:^[a-zA-Z0-9_-]+$",
"group": "regex:^[a-zA-Z0-9_-]+$",
+34
View File
@@ -0,0 +1,34 @@
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:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"content": "required|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
}
+1 -1
View File
@@ -16,7 +16,7 @@ func (r *Search) Authorize(ctx http.Context) error {
func (r *Search) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$|path_exists",
"path": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"keyword": "required|string",
}
}
+2 -2
View File
@@ -16,8 +16,8 @@ func (r *UnArchive) Authorize(ctx http.Context) error {
func (r *UnArchive) Rules(ctx http.Context) map[string]string {
return map[string]string{
"file": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$|path_exists",
"path": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$|path_not_exists",
"file": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_exists`,
"path": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$|path_not_exists`,
}
}
+36
View File
@@ -0,0 +1,36 @@
package requests
import (
"mime/multipart"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/contracts/validation"
)
type Upload struct {
Path string `form:"path" json:"path"`
File *multipart.FileHeader `form:"file" json:"file"`
}
func (r *Upload) Authorize(ctx http.Context) error {
return nil
}
func (r *Upload) Rules(ctx http.Context) map[string]string {
return map[string]string{
"path": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$`,
"file": "required",
}
}
func (r *Upload) Messages(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Upload) Attributes(ctx http.Context) map[string]string {
return map[string]string{}
}
func (r *Upload) PrepareForValidation(ctx http.Context, data validation.Data) error {
return nil
}
+1 -1
View File
@@ -21,7 +21,7 @@ func (r *Create) Authorize(ctx http.Context) error {
func (r *Create) Rules(ctx http.Context) map[string]string {
return map[string]string{
"name": "required|regex:^[a-zA-Z0-9-_]+$",
"path": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$",
"path": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$`,
"comment": "string",
"auth_user": "required|regex:^[a-zA-Z0-9-_]+$",
"secret": "required|min_len:8",
+1 -1
View File
@@ -21,7 +21,7 @@ func (r *Update) Authorize(ctx http.Context) error {
func (r *Update) Rules(ctx http.Context) map[string]string {
return map[string]string{
"name": "required|regex:^[a-zA-Z0-9-_]+$",
"path": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$",
"path": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$`,
"comment": "string",
"auth_user": "required|regex:^[a-zA-Z0-9-_]+$",
"secret": "required|min_len:8",
+2
View File
@@ -11,6 +11,7 @@ type Update struct {
BackupPath string `form:"backup_path" json:"backup_path"`
WebsitePath string `form:"website_path" json:"website_path"`
Entrance string `form:"entrance" json:"entrance"`
SSL bool `form:"ssl" json:"ssl"`
UserName string `form:"username" json:"username"`
Email string `form:"email" json:"email"`
Password string `form:"password" json:"password"`
@@ -27,6 +28,7 @@ func (r *Update) Rules(ctx http.Context) map[string]string {
"backup_path": "required|string:2,255",
"website_path": "required|string:2,255",
"entrance": `required|regex:^/(\w+)?$|not_in:/api`,
"ssl": "bool",
"username": "required|string:2,20",
"email": "required|email",
"password": "string:8,255",
+2 -2
View File
@@ -10,7 +10,7 @@ type Add struct {
Domains []string `form:"domains" json:"domains"`
Ports []uint `form:"ports" json:"ports"`
Path string `form:"path" json:"path"`
Php int `form:"php" json:"php"`
Php string `form:"php" json:"php"`
Db bool `form:"db" json:"db"`
DbType string `form:"db_type" json:"db_type"`
DbName string `form:"db_name" json:"db_name"`
@@ -27,7 +27,7 @@ func (r *Add) Rules(ctx http.Context) map[string]string {
"name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$|not_exists:websites,name|not_in:phpmyadmin,mysql,panel,ssh",
"domains": "required|slice",
"ports": "required|slice",
"path": "regex:^/[a-zA-Z0-9_-]+(\\/[a-zA-Z0-9_-]+)*$",
"path": `regex:^/[a-zA-Z0-9_.@#$%\-\s\[\]()]+(/[a-zA-Z0-9_.@#$%\-\s\[\]()]+)*$`,
"php": "required",
"db": "bool",
"db_type": "required_if:db,true|in:0,mysql,postgresql",
-8
View File
@@ -1,8 +0,0 @@
package responses
import "panel/app/models"
type CertList struct {
Total int64 `json:"total"`
Items []models.Cert `json:"items"`
}
-8
View File
@@ -1,8 +0,0 @@
package responses
import "panel/app/models"
type DNSList struct {
Total int64 `json:"total"`
Items []models.CertDNS `json:"items"`
}
-8
View File
@@ -1,8 +0,0 @@
package responses
import "panel/app/models"
type UserList struct {
Total int64 `json:"total"`
Items []models.CertUser `json:"items"`
}
-12
View File
@@ -1,12 +0,0 @@
package responses
type Settings struct {
Name string `json:"name"`
Username string `json:"username"`
Password string `json:"password"`
Email string `json:"email"`
Port string `json:"port"`
Entrance string `json:"entrance"`
WebsitePath string `json:"website_path"`
BackupPath string `json:"backup_path"`
}
-8
View File
@@ -1,8 +0,0 @@
package responses
type Info struct {
ID uint `json:"id"`
Role []string `json:"role"`
Username string `json:"username"`
Email string `json:"email"`
}
-8
View File
@@ -1,8 +0,0 @@
package responses
import "panel/app/models"
type List struct {
Total int64 `json:"total"`
Items []models.Website `json:"items"`
}
+3 -3
View File
@@ -6,9 +6,9 @@ import (
type Website struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"unique;not null" json:"name"`
Status bool `gorm:"default:true;not null;index" json:"status"`
Path string `gorm:"not null" json:"path"`
Name string `json:"name"`
Status bool `gorm:"default:true" json:"status"`
Path string `json:"path"`
Php int `gorm:"default:0;not null;index" json:"php"`
Ssl bool `gorm:"default:false;not null;index" json:"ssl"`
Remark string `gorm:"default:''" json:"remark"`
-13
View File
@@ -1,13 +0,0 @@
package fail2ban
var (
Name = "Fail2ban"
Description = "Fail2ban 扫描系统日志文件并从中找出多次尝试失败的IP地址,将该IP地址加入防火墙的拒绝访问列表中。"
Slug = "fail2ban"
Version = "1.0.0"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/fail2ban/install.sh`
Uninstall = `bash /www/panel/scripts/fail2ban/uninstall.sh`
Update = `bash /www/panel/scripts/fail2ban/update.sh`
)
-13
View File
@@ -1,13 +0,0 @@
package mysql57
var (
Name = "MySQL-5.7"
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。"
Slug = "mysql57"
Version = "5.7.44"
Requires = []string{}
Excludes = []string{"mysql80"}
Install = `bash /www/panel/scripts/mysql/install.sh 57`
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 57`
Update = `bash /www/panel/scripts/mysql/update.sh 57`
)
-13
View File
@@ -1,13 +0,0 @@
package mysql80
var (
Name = "MySQL-8.0"
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。"
Slug = "mysql80"
Version = "8.0.35"
Requires = []string{}
Excludes = []string{"mysql57"}
Install = `bash /www/panel/scripts/mysql/install.sh 80`
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 80`
Update = `bash /www/panel/scripts/mysql/update.sh 80`
)
-13
View File
@@ -1,13 +0,0 @@
package openresty
var (
Name = "OpenResty"
Description = "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。"
Slug = "openresty"
Version = "1.25.3.1rc1"
Requires = []string{}
Excludes = []string{}
Install = "bash /www/panel/scripts/openresty/install.sh"
Uninstall = "bash /www/panel/scripts/openresty/uninstall.sh"
Update = "bash /www/panel/scripts/openresty/install.sh"
)
-13
View File
@@ -1,13 +0,0 @@
package php74
var (
Name = "PHP-7.4"
Description = "PHP 是世界上最好的语言!"
Slug = "php74"
Version = "7.4.33"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/php/install.sh 74`
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 74`
Update = `bash /www/panel/scripts/php/install.sh 74`
)
-13
View File
@@ -1,13 +0,0 @@
package php80
var (
Name = "PHP-8.0"
Description = "PHP 是世界上最好的语言!"
Slug = "php80"
Version = "8.0.30"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/php/install.sh 80`
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 80`
Update = `bash /www/panel/scripts/php/install.sh 80`
)
-13
View File
@@ -1,13 +0,0 @@
package php81
var (
Name = "PHP-8.1"
Description = "PHP 是世界上最好的语言!"
Slug = "php81"
Version = "8.1.26"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/php/install.sh 81`
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 81`
Update = `bash /www/panel/scripts/php/install.sh 81`
)
-13
View File
@@ -1,13 +0,0 @@
package php82
var (
Name = "PHP-8.2"
Description = "PHP 是世界上最好的语言!"
Slug = "php82"
Version = "8.2.13"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/php/install.sh 82`
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 82`
Update = `bash /www/panel/scripts/php/install.sh 82`
)
-13
View File
@@ -1,13 +0,0 @@
package php83
var (
Name = "PHP-8.3"
Description = "PHP 是世界上最好的语言!"
Slug = "php83"
Version = "8.3.0"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/php/install.sh 83`
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 83`
Update = `bash /www/panel/scripts/php/install.sh 83`
)
-13
View File
@@ -1,13 +0,0 @@
package phpmyadmin
var (
Name = "phpMyAdmin"
Description = "phpMyAdmin 是一个以 PHP 为基础,以 Web-Base 方式架构在网站主机上的 MySQL 数据库管理工具。"
Slug = "phpmyadmin"
Version = "5.2.1"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/phpmyadmin/install.sh`
Uninstall = `bash /www/panel/scripts/phpmyadmin/uninstall.sh`
Update = `bash /www/panel/scripts/phpmyadmin/uninstall.sh && bash /www/panel/scripts/phpmyadmin/install.sh`
)
-13
View File
@@ -1,13 +0,0 @@
package postgresql15
var (
Name = "PostgreSQL-15"
Description = "PostgreSQL 是世界上最先进的开源关系数据库,在类似 BSD 与 MIT 许可的 PostgreSQL 许可下发行。"
Slug = "postgresql15"
Version = "15.5"
Requires = []string{}
Excludes = []string{"postgresql16"}
Install = `bash /www/panel/scripts/postgresql/install.sh 15`
Uninstall = `bash /www/panel/scripts/postgresql/uninstall.sh 15`
Update = `bash /www/panel/scripts/postgresql/update.sh 15`
)
-13
View File
@@ -1,13 +0,0 @@
package postgresql16
var (
Name = "PostgreSQL-16"
Description = "PostgreSQL 是世界上最先进的开源关系数据库,在类似 BSD 与 MIT 许可的 PostgreSQL 许可下发行。"
Slug = "postgresql16"
Version = "16.1"
Requires = []string{}
Excludes = []string{"postgresql15"}
Install = `bash /www/panel/scripts/postgresql/install.sh 16`
Uninstall = `bash /www/panel/scripts/postgresql/uninstall.sh 16`
Update = `bash /www/panel/scripts/postgresql/update.sh 16`
)
-13
View File
@@ -1,13 +0,0 @@
package pureftpd
var (
Name = "Pure-FTPd"
Description = "Pure-Ftpd 是一个快速、高效、轻便、安全的 FTP 服务器,它以安全和配置简单为设计目标,支持虚拟主机,IPV6,PAM 等功能。"
Slug = "pureftpd"
Version = "1.0.50"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/pureftpd/install.sh`
Uninstall = `bash /www/panel/scripts/pureftpd/uninstall.sh`
Update = `bash /www/panel/scripts/pureftpd/update.sh`
)
-13
View File
@@ -1,13 +0,0 @@
package redis
var (
Name = "Redis"
Description = "Redis 是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。"
Slug = "redis"
Version = "7.2.3"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/redis/install.sh`
Uninstall = `bash /www/panel/scripts/redis/uninstall.sh`
Update = `bash /www/panel/scripts/redis/update.sh`
)
-13
View File
@@ -1,13 +0,0 @@
package rsync
var (
Name = "Rsync"
Description = "Rsync 是一款提供快速增量文件传输的开源工具。"
Slug = "rsync"
Version = "3.2.7"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/rsync/install.sh`
Uninstall = `bash /www/panel/scripts/rsync/uninstall.sh`
Update = `bash /www/panel/scripts/rsync/install.sh`
)
-13
View File
@@ -1,13 +0,0 @@
package s3fs
var (
Name = "S3fs"
Description = "S3fs 通过 FUSE 挂载兼容 S3 标准的存储桶,例如Amazon S3、阿里云OSS、腾讯云COS、七牛云Kodo等。"
Slug = "s3fs"
Version = "1.9"
Requires = []string{}
Excludes = []string{}
Install = `bash /www/panel/scripts/s3fs/install.sh`
Uninstall = `bash /www/panel/scripts/s3fs/uninstall.sh`
Update = `bash /www/panel/scripts/s3fs/update.sh`
)

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