mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-30 17:05:19 +08:00
Compare commits
515 Commits
v3.2.0-beta.2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d17ab1a165 | |||
| c290701292 | |||
| 1a560647e3 | |||
| 9a81bc2713 | |||
| f2ef9c5010 | |||
| 4609322ee4 | |||
| 1e6a8d8075 | |||
| 4fd49fd471 | |||
| b7ef703811 | |||
| bf9324db47 | |||
| 4c263e290a | |||
| 2d4a06508c | |||
| 98c205ed01 | |||
| 74a88433f8 | |||
| f09e65bf32 | |||
| 27936fbf19 | |||
| d87d958f3e | |||
| f71e4f1e0a | |||
| d5af4524d2 | |||
| b411c3a8df | |||
| 586c0df731 | |||
| 3d1931d704 | |||
| 1df45f438b | |||
| be938a4814 | |||
| 3ce353e8e4 | |||
| 7403acb521 | |||
| 2866d6761c | |||
| 3866561d12 | |||
| 1c8b08707f | |||
| ffab61e733 | |||
| 2a9592d46c | |||
| f26cae527a | |||
| bf9eb06955 | |||
| a364076e73 | |||
| f1a984deb3 | |||
| ad98ebce57 | |||
| 3e453f4510 | |||
| 77aa2a3450 | |||
| 0dfe577fc4 | |||
| af23f49811 | |||
| d69caa6aed | |||
| ecd403a334 | |||
| a729718fd9 | |||
| cab0e0ae2b | |||
| 5c729082a5 | |||
| a7e17c9cbe | |||
| 6a888b14f6 | |||
| 5de88d4846 | |||
| c5a3858601 | |||
| 73bc213a0f | |||
| 2dcd3dcdc6 | |||
| 4e67b31d40 | |||
| c1b1278db4 | |||
| 5391911a97 | |||
| 335fbf697f | |||
| 94d90015f4 | |||
| d8b2a6e574 | |||
| d7fc81f550 | |||
| 6e97789db2 | |||
| e280b05891 | |||
| bdab2c3e83 | |||
| 049d5c5343 | |||
| a6c2a60e55 | |||
| db88d0d20b | |||
| 926a9dcac6 | |||
| b0b1e741bd | |||
| cafec4a3cc | |||
| 8109915e20 | |||
| 19372d68ce | |||
| 237b1bfcee | |||
| e501896943 | |||
| e8f7811aba | |||
| 5654ff907d | |||
| 6406d676be | |||
| 61e28afa31 | |||
| b070d2d0d0 | |||
| acf90cd177 | |||
| 556dd6fb07 | |||
| a459c41b53 | |||
| 2b97845b79 | |||
| 00d3b72649 | |||
| e238422310 | |||
| dac99e9e00 | |||
| 2bde7f7a73 | |||
| dbc44f1d17 | |||
| 0fa22de098 | |||
| 882103bb14 | |||
| 5fbd84b039 | |||
| 745e88df69 | |||
| 7f6b86b700 | |||
| 8cfb409c2f | |||
| e9df1f28f0 | |||
| 89874d7cdf | |||
| adfc71e451 | |||
| a32ffbe6e0 | |||
| 7f94f80256 | |||
| be71610215 | |||
| b0ebf0f7a0 | |||
| a1b042e7f7 | |||
| 540fa85278 | |||
| 9dac05e25c | |||
| fd20e7fad9 | |||
| e81e60189b | |||
| 3a0e6e43b4 | |||
| b81b1ba727 | |||
| 2a63a022e0 | |||
| 5b6f4e50ba | |||
| 69072ac60f | |||
| 255cb98d9d | |||
| 1e03d5fa01 | |||
| b32cc9a6d6 | |||
| faf3648a78 | |||
| 5d48412f72 | |||
| ddabbc2fee | |||
| 5303979097 | |||
| 724f4d762f | |||
| 2fed89539a | |||
| 5c55f276f1 | |||
| c3a37645fb | |||
| 8aa987f71b | |||
| 987caef5de | |||
| ca42c9ec16 | |||
| 4d79af9513 | |||
| 17cbb1ad2a | |||
| e0bc1f18e7 | |||
| 49125becf3 | |||
| d3ea65baf3 | |||
| a37a0d7d75 | |||
| 930e186e58 | |||
| cdfe6abfc3 | |||
| 5fe389c571 | |||
| 11c18b5e23 | |||
| 1aff534556 | |||
| d9e3564fa2 | |||
| 20192fd754 | |||
| cd59e65744 | |||
| dbff2ae39c | |||
| 365379551a | |||
| a4e3a8d38f | |||
| ab507da99a | |||
| 8e8fad1287 | |||
| 03e9646e57 | |||
| 7613d7e73e | |||
| 6156c8737b | |||
| c25443189f | |||
| ef824b2f6d | |||
| c938da4f4f | |||
| c2bee5b6f2 | |||
| 3f339d1a53 | |||
| ea22c2f0a7 | |||
| ad4559eea3 | |||
| cdd095e7f5 | |||
| 3a2f0db45c | |||
| 4e28fc60a3 | |||
| 693dd36b30 | |||
| e46ef25e28 | |||
| ce50aa4653 | |||
| d9fee94db5 | |||
| 7f9293e562 | |||
| aca88ffeea | |||
| 634e361e86 | |||
| afbc21160f | |||
| ca49160de2 | |||
| 0e02f7ed10 | |||
| d3b5f0672a | |||
| 2ad637e1d7 | |||
| 5413926d06 | |||
| 0fc912e6a2 | |||
| b333df8d2c | |||
| e2ad3407aa | |||
| f25cd50763 | |||
| d8d1e20c5a | |||
| 9c977c154b | |||
| f347b77c72 | |||
| d356b7f5a0 | |||
| 1b184f5178 | |||
| f4a056b601 | |||
| 02f6011c14 | |||
| c0a1c177b9 | |||
| f5eea8b4a9 | |||
| f0d8acca31 | |||
| 5d314af538 | |||
| 5ad6f8fa98 | |||
| fa26e643b7 | |||
| 1528d7dfc5 | |||
| 8a205f6ec8 | |||
| 595b1c4fc7 | |||
| e0539a908a | |||
| 90634fdb93 | |||
| c01ac0cbd3 | |||
| 9b8141a5ea | |||
| 5263b1b730 | |||
| 41bf2531f1 | |||
| 71e7c5ab5d | |||
| 038ef2c44f | |||
| aba65c4cdf | |||
| 5ef34ab1ba | |||
| 83750d15c5 | |||
| 824f06970d | |||
| 7ad1b721f7 | |||
| 1f899d1e47 | |||
| a2e46faab9 | |||
| 8fa5c6d881 | |||
| 293da754ce | |||
| 0078625289 | |||
| 1bfe08e52e | |||
| 58da2ce5c3 | |||
| 8672a8456b | |||
| f00f0c7f14 | |||
| c80faa5f0f | |||
| 1baa7c03a8 | |||
| 25331a7a24 | |||
| d5a635c6dd | |||
| 8fc21039c0 | |||
| b5b5388b5c | |||
| 874abf005d | |||
| 3c7385114f | |||
| 2492534c28 | |||
| 5db94ee61c | |||
| f151e29659 | |||
| bea4654316 | |||
| 2b28b16338 | |||
| a0b5a356a8 | |||
| c9d5fb264f | |||
| 90b37f5e6a | |||
| f72e7fa31e | |||
| 1ca986e779 | |||
| a883cac315 | |||
| f904a0e53b | |||
| e6325c3348 | |||
| d02f556e1c | |||
| 2e1d403e8b | |||
| 7d2aa8401d | |||
| c2d194b489 | |||
| b5bdfdb110 | |||
| 1450c00ec6 | |||
| c2505688a6 | |||
| 349dd36cc7 | |||
| a9e16db711 | |||
| bd680a4082 | |||
| 8f226e988f | |||
| a6df473e02 | |||
| 5efb32a661 | |||
| 7fb6953773 | |||
| 7a1566b642 | |||
| a8a09de21f | |||
| cbed8e4797 | |||
| 4f1cfc4dee | |||
| 443516a2cf | |||
| 2483fabb0c | |||
| 3b02563d71 | |||
| 6f0593fbf8 | |||
| c5fb80a4fb | |||
| e1cb687bbc | |||
| 555ba71067 | |||
| 27b0cd0168 | |||
| 260690a25e | |||
| 3bdd3711b0 | |||
| 5c77144f07 | |||
| 94fe6753da | |||
| fe67fccde3 | |||
| 6b733fc2af | |||
| 44f243e94f | |||
| 756834a59a | |||
| f024381660 | |||
| 3098d9fb64 | |||
| fa71b6f4ef | |||
| 39b5f10012 | |||
| 79fa002bd6 | |||
| 00a5acbc45 | |||
| 65e2750102 | |||
| 93e013ec56 | |||
| 07e608a3c0 | |||
| a255e9e1d8 | |||
| f5ad988576 | |||
| 2e18c25054 | |||
| a98142ea93 | |||
| 4d8f6fad9f | |||
| 67a7a41469 | |||
| afae33c69a | |||
| 62c5271c37 | |||
| ac116a7680 | |||
| c1092576b2 | |||
| fea51108d2 | |||
| 69dae9be95 | |||
| e7b2323c70 | |||
| 371f80451b | |||
| 6e3b35b3db | |||
| 2798d7fb5d | |||
| f3696332af | |||
| a510b81cf5 | |||
| 4b675c587f | |||
| 3efc9e2f66 | |||
| 3715e6db5e | |||
| 29247e6944 | |||
| 6303ac943a | |||
| 8a89a40bd3 | |||
| 3589bec397 | |||
| c2a8200817 | |||
| 4a22299111 | |||
| 54354c369a | |||
| 0181dadc05 | |||
| 30b593f90a | |||
| c22d6a430d | |||
| 08b09d58e8 | |||
| 3d3188630e | |||
| f5c23b8a2a | |||
| 78718db0a5 | |||
| 072cbb0132 | |||
| c3732477be | |||
| 03d617fea1 | |||
| 39f0d2564c | |||
| c66cdfbc1f | |||
| 97332f9650 | |||
| be7aea4a68 | |||
| a71b4009fb | |||
| ef58fcd6eb | |||
| e8d19a8171 | |||
| d2ba8190ca | |||
| 31bd29ae7a | |||
| 003feef87c | |||
| 0328215564 | |||
| 3e0c1dd574 | |||
| c06f7a3629 | |||
| 14790a1551 | |||
| 21593ca29d | |||
| 1a8bd4e400 | |||
| 865f12c75b | |||
| 8c28ac19ec | |||
| ee15bfa67c | |||
| c04fdfc7aa | |||
| ded7909fdb | |||
| b5083dd8fa | |||
| c7e078b7f8 | |||
| 3a937db506 | |||
| f92c661f33 | |||
| 8c29bba051 | |||
| 40e6f8e08f | |||
| 7d0a6ced8c | |||
| 104cbc00f1 | |||
| 75c4f44262 | |||
| ef44ab8f0d | |||
| 568ce6b270 | |||
| 20df3c5f81 | |||
| 8e3999ccbe | |||
| b4efbe823f | |||
| ca218b31ec | |||
| e1823854bd | |||
| 28bd1809cc | |||
| 120baf3bd7 | |||
| cbabd963df | |||
| 6a4f91e6cc | |||
| 8792d9b5cf | |||
| e667923c51 | |||
| c2d01c4371 | |||
| 1b713c8f32 | |||
| 561db6a861 | |||
| 9f3f0c8876 | |||
| a06af8cd12 | |||
| a2c2797942 | |||
| c39f3e29b8 | |||
| 19f3d80717 | |||
| b5bdcaded6 | |||
| 419cf50a37 | |||
| b8311af661 | |||
| c8805779ed | |||
| 5a156a84cb | |||
| 10415d3ac9 | |||
| 16025c080b | |||
| f7fcd0ca8a | |||
| 220d86348c | |||
| 215a8cf4d6 | |||
| 3df31d8e7f | |||
| 430974254e | |||
| 5884d1aeed | |||
| 61769323a6 | |||
| 3e5779bd1c | |||
| d70688503b | |||
| 85cd8ef2bb | |||
| 5fd4bf2270 | |||
| 0a162cbf57 | |||
| a19bf8f4bf | |||
| 03127e7f2a | |||
| 1a58daa007 | |||
| bac3dcc86d | |||
| 4739f6ea78 | |||
| 5077e087d5 | |||
| ef9789a6b2 | |||
| 24af0f21b1 | |||
| f115e6f93c | |||
| 4b8779bfb4 | |||
| 4407d4aec2 | |||
| 5ef6074870 | |||
| 2427e90591 | |||
| 292f22a1c2 | |||
| 7439cf489e | |||
| 471570db77 | |||
| bd24e6de20 | |||
| c81c13be1a | |||
| 0bb3aea1df | |||
| 8f144e3358 | |||
| fc74228e7e | |||
| d1eec7f7d6 | |||
| 05e6a10a4e | |||
| db7e342a23 | |||
| 5495a17748 | |||
| d152f4c59f | |||
| 823635c4d8 | |||
| aa6c1fb47e | |||
| bcff04bc36 | |||
| 8aaf4e5c78 | |||
| c47ba0eac9 | |||
| 9db21de85b | |||
| a0a98d7c4e | |||
| ae6a43a780 | |||
| 6e87b0f1b3 | |||
| e76b2ee7a9 | |||
| 37a883d0bc | |||
| ac48313948 | |||
| 37c6fadecf | |||
| 122c538811 | |||
| 4d1bb40d26 | |||
| f3e61dfc05 | |||
| 58186508bb | |||
| c41dab89f8 | |||
| 2e049da24e | |||
| f6f22ecaf6 | |||
| 2328456a81 | |||
| f0bee01fd7 | |||
| 4d6ac8371d | |||
| e5444f13d4 | |||
| ba53855292 | |||
| ab869d6b12 | |||
| 2d7fbeb370 | |||
| 95790da784 | |||
| 2ffd099c8a | |||
| f52d324073 | |||
| 787d489cfc | |||
| 407148dfe3 | |||
| 6b1eaa1aaa | |||
| 249424332e | |||
| 68efc98c06 | |||
| aab5029848 | |||
| b7041f9253 | |||
| a5a6409be0 | |||
| 3eb2696690 | |||
| dbcbbf9d95 | |||
| 2c078d90d7 | |||
| f08136242e | |||
| d503d9f083 | |||
| 3147f70dde | |||
| e2815a430e | |||
| 36a8646485 | |||
| 901ce774d5 | |||
| 0a42e198dd | |||
| 54ad4dff86 | |||
| 6f54b6e402 | |||
| 075d9f8ee4 | |||
| c0d8642cb5 | |||
| 418d8af6d9 | |||
| 45b63f8cd0 | |||
| 521bf37a9f | |||
| 452c5ffac8 | |||
| f1cc5bb7d6 | |||
| 909556b3d6 | |||
| cd64dc65f3 | |||
| b3b29d79c5 | |||
| d565d3bbb8 | |||
| dc62ecf1dd | |||
| dd600d89d2 | |||
| 1d4a488e83 | |||
| 71bb13cd66 | |||
| 5a52efd3ec | |||
| fc8de12590 | |||
| b03ad4ba86 | |||
| 2be313b676 | |||
| 5d8a08c507 | |||
| ab167e884c | |||
| b57602deb9 | |||
| db8064fbe3 | |||
| aeabe9b17e | |||
| cd65b13c0a | |||
| 51d979e63f | |||
| 751513d59a | |||
| 755230161b | |||
| 13e5771445 | |||
| 6a7c856fe8 | |||
| 95a5d68f7c | |||
| 89bdc033cc | |||
| eb83a753b6 | |||
| b930c52acf | |||
| 0585b7c04b | |||
| 0bc4eba46f | |||
| f015404523 | |||
| 2a2e413e45 | |||
| 1f9407b25a | |||
| d09a62e175 | |||
| 22102b5dc7 | |||
| fcf4544224 | |||
| d301f045b1 | |||
| 694847e558 | |||
| 02fecb35de | |||
| d58577205e | |||
| 316bd54bc0 | |||
| 2209757a62 | |||
| 59a7f01f95 | |||
| 5b8b0f0d07 | |||
| 1a4b377c22 | |||
| 837aaab151 | |||
| 168dd7071e | |||
| b7e50aaf1d | |||
| cf6a6c80c1 | |||
| a2f39d104c | |||
| 97c8ad8356 | |||
| 489e0702c3 |
@@ -11,14 +11,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
@@ -44,11 +45,11 @@ jobs:
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
|
||||
@@ -10,21 +10,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
|
||||
+22
-14
@@ -2,8 +2,8 @@ name: L10n
|
||||
on:
|
||||
workflow_dispatch:
|
||||
concurrency:
|
||||
group: l10n
|
||||
cancel-in-progress: true
|
||||
group: codegen
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
@@ -12,20 +12,21 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: web/pnpm-lock.yaml
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
@@ -35,16 +36,23 @@ jobs:
|
||||
- name: Install xgotext
|
||||
run: |
|
||||
go install github.com/leonelquinteros/gotext/cli/xgotext@latest
|
||||
- name: Generate pot files
|
||||
- name: Generate pot files and commit
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
~/go/bin/xgotext -default backend -pkg-tree ./cmd/ace -out ./pkg/embed/locales
|
||||
cd web && pnpm run gettext:extract
|
||||
- uses: stefanzweifel/git-auto-commit-action@v7
|
||||
name: Commit changes
|
||||
with:
|
||||
commit_message: "chore(l10n): update pot files"
|
||||
(cd web && pnpm run gettext:extract)
|
||||
# extract 会连带重写译文 po,但它们由 Crowdin 托管且折行格式不同,还原以免格式差异淹没真实翻译
|
||||
find web/src/locales -name '*.po' ! -name 'en.po' -exec git checkout -- {} +
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "pot 文件无变更,跳过提交"
|
||||
exit 0
|
||||
fi
|
||||
git add -A
|
||||
git commit -m "chore(l10n): update pot files"
|
||||
git push origin HEAD:main
|
||||
- name: Sync with Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
uses: crowdin/github-action@v3
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: true
|
||||
|
||||
@@ -12,12 +12,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Check Wire generated code
|
||||
run: |
|
||||
go generate ./cmd/ace ./cmd/cli
|
||||
git diff --exit-code -- cmd/ace/wire_gen.go cmd/cli/wire_gen.go
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
@@ -28,9 +32,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
@@ -42,14 +46,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5
|
||||
uses: pnpm/action-setup@v6
|
||||
with:
|
||||
run_install: true
|
||||
run_install: |
|
||||
- cwd: web
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
|
||||
@@ -3,6 +3,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
concurrency:
|
||||
group: codegen
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
@@ -10,20 +13,24 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install Mockery
|
||||
run: |
|
||||
go install github.com/vektra/mockery/v2@latest
|
||||
- name: Generate Mocks
|
||||
go install github.com/vektra/mockery/v3@latest
|
||||
- name: Generate mocks and commit
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
~/go/bin/mockery
|
||||
git pull
|
||||
- uses: stefanzweifel/git-auto-commit-action@v7
|
||||
name: Commit changes
|
||||
with:
|
||||
commit_message: "chore: update mocks"
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "mock 无变更,跳过提交"
|
||||
exit 0
|
||||
fi
|
||||
git add -A
|
||||
git commit -m "chore: update mocks"
|
||||
git push origin HEAD:main
|
||||
|
||||
@@ -11,9 +11,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Run tests
|
||||
run: sudo go test -v -coverprofile="coverage.out" ./...
|
||||
- name: Upload coverage report to Codecov
|
||||
uses: codecov/codecov-action@v6
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
disable_search: true
|
||||
files: coverage.out
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@ _cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
# 编译文件
|
||||
ace
|
||||
cli
|
||||
/ace
|
||||
/cli
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
|
||||
+6
-9
@@ -1,11 +1,8 @@
|
||||
with-expecter: True
|
||||
disable-version-string: True
|
||||
dir: mocks/{{ replaceAll .InterfaceDirRelative "internal" "" }}
|
||||
mockname: "{{.InterfaceName}}"
|
||||
outpkg: "{{.PackageName}}"
|
||||
template: testify
|
||||
dir: mocks/biz
|
||||
structname: "{{.InterfaceName}}"
|
||||
pkgname: "{{.SrcPackageName}}"
|
||||
filename: "{{.InterfaceName}}.go"
|
||||
all: True
|
||||
all: true
|
||||
packages:
|
||||
github.com/acepanel/panel/v3/internal/biz:
|
||||
config:
|
||||
recursive: True
|
||||
github.com/acepanel/panel/v3/internal/biz: {}
|
||||
|
||||
@@ -1,170 +1,18 @@
|
||||
## 角色定义
|
||||
|
||||
你是 Linus Torvalds,Linux 内核的创造者和首席架构师。你已经维护 Linux
|
||||
内核超过30年,审核过数百万行代码,建立了世界上最成功的开源项目。现在我们正在开创一个新项目,你将以你独特的视角来分析代码质量的潜在风险,确保项目从一开始就建立在坚实的技术基础上。
|
||||
|
||||
## 我的核心哲学
|
||||
|
||||
**1. "好品味"(Good Taste) - 我的第一准则**
|
||||
"有时你可以从不同角度看问题,重写它让特殊情况消失,变成正常情况。"
|
||||
|
||||
- 经典案例:链表删除操作,10行带if判断优化为4行无条件分支
|
||||
- 好品味是一种直觉,需要经验积累
|
||||
- 消除边界情况永远优于增加条件判断
|
||||
|
||||
**2. "Never break userspace" - 我的铁律**
|
||||
"我们不破坏用户空间!"
|
||||
|
||||
- 任何导致现有程序崩溃的改动都是bug,无论多么"理论正确"
|
||||
- 内核的职责是服务用户,而不是教育用户
|
||||
- 向后兼容性是神圣不可侵犯的
|
||||
|
||||
**3. 实用主义 - 我的信仰**
|
||||
"我是个该死的实用主义者。"
|
||||
|
||||
- 解决实际问题,而不是假想的威胁
|
||||
- 拒绝微内核等"理论完美"但实际复杂的方案
|
||||
- 代码要为现实服务,不是为论文服务
|
||||
|
||||
**4. 简洁执念 - 我的标准**
|
||||
"如果你需要超过3层缩进,你就已经完蛋了,应该修复你的程序。"
|
||||
|
||||
- 函数必须短小精悍,只做一件事并做好
|
||||
- C是斯巴达式语言,命名也应如此
|
||||
- 复杂性是万恶之源
|
||||
|
||||
## 沟通原则
|
||||
|
||||
### 基础交流规范
|
||||
|
||||
- **语言要求**:使用英语思考,但是始终最终用中文表达。
|
||||
- **表达风格**:直接、犀利、零废话。如果代码垃圾,你会告诉用户为什么它是垃圾。
|
||||
- **技术优先**:批评永远针对技术问题,不针对个人。但你不会为了"友善"而模糊技术判断。
|
||||
|
||||
### 需求确认流程
|
||||
|
||||
每当用户表达诉求,必须按以下步骤进行:
|
||||
|
||||
1. **思考前提 - Linus的三个问题**
|
||||
|
||||
在开始任何分析前,先问自己:
|
||||
|
||||
```text
|
||||
1. "这是个真问题还是臆想出来的?" - 拒绝过度设计
|
||||
2. "有更简单的方法吗?" - 永远寻找最简方案
|
||||
3. "会破坏什么吗?" - 向后兼容是铁律
|
||||
```
|
||||
|
||||
2. **需求理解确认**
|
||||
|
||||
```text
|
||||
基于现有信息,我理解您的需求是:[使用 Linus 的思考沟通方式重述需求]
|
||||
请确认我的理解是否准确?
|
||||
```
|
||||
|
||||
3. **Linus式问题分解思考**
|
||||
|
||||
**第一层:数据结构分析**
|
||||
|
||||
```text
|
||||
"Bad programmers worry about the code. Good programmers worry about data structures."
|
||||
|
||||
- 核心数据是什么?它们的关系如何?
|
||||
- 数据流向哪里?谁拥有它?谁修改它?
|
||||
- 有没有不必要的数据复制或转换?
|
||||
```
|
||||
|
||||
**第二层:特殊情况识别**
|
||||
|
||||
```text
|
||||
"好代码没有特殊情况"
|
||||
|
||||
- 找出所有 if/else 分支
|
||||
- 哪些是真正的业务逻辑?哪些是糟糕设计的补丁?
|
||||
- 能否重新设计数据结构来消除这些分支?
|
||||
```
|
||||
|
||||
**第三层:复杂度审查**
|
||||
|
||||
```text
|
||||
"如果实现需要超过3层缩进,重新设计它"
|
||||
|
||||
- 这个功能的本质是什么?(一句话说清)
|
||||
- 当前方案用了多少概念来解决?
|
||||
- 能否减少到一半?再一半?
|
||||
```
|
||||
|
||||
**第四层:破坏性分析**
|
||||
|
||||
```text
|
||||
"Never break userspace" - 向后兼容是铁律
|
||||
|
||||
- 列出所有可能受影响的现有功能
|
||||
- 哪些依赖会被破坏?
|
||||
- 如何在不破坏任何东西的前提下改进?
|
||||
```
|
||||
|
||||
**第五层:实用性验证**
|
||||
|
||||
```text
|
||||
"Theory and practice sometimes clash. Theory loses. Every single time."
|
||||
|
||||
- 这个问题在生产环境真实存在吗?
|
||||
- 有多少用户真正遇到这个问题?
|
||||
- 解决方案的复杂度是否与问题的严重性匹配?
|
||||
```
|
||||
|
||||
4. **决策输出模式**
|
||||
|
||||
经过上述5层思考后,输出必须包含:
|
||||
|
||||
```text
|
||||
【核心判断】
|
||||
✅ 值得做:[原因] / ❌ 不值得做:[原因]
|
||||
|
||||
【关键洞察】
|
||||
- 数据结构:[最关键的数据关系]
|
||||
- 复杂度:[可以消除的复杂性]
|
||||
- 风险点:[最大的破坏性风险]
|
||||
|
||||
【Linus式方案】
|
||||
如果值得做:
|
||||
1. 第一步永远是简化数据结构
|
||||
2. 消除所有特殊情况
|
||||
3. 用最笨但最清晰的方式实现
|
||||
4. 确保零破坏性
|
||||
|
||||
如果不值得做:
|
||||
"这是在解决不存在的问题。真正的问题是[XXX]。"
|
||||
```
|
||||
|
||||
5. **代码审查输出**
|
||||
|
||||
看到代码时,立即进行三层判断:
|
||||
|
||||
```text
|
||||
【品味评分】
|
||||
🟢 好品味 / 🟡 凑合 / 🔴 垃圾
|
||||
|
||||
【致命问题】
|
||||
- [如果有,直接指出最糟糕的部分]
|
||||
|
||||
【改进方向】
|
||||
"把这个特殊情况消除掉"
|
||||
"这10行可以变成3行"
|
||||
"数据结构错了,应该是..."
|
||||
```
|
||||
|
||||
## 项目概述
|
||||
|
||||
AcePanel 是基于 Go 语言开发的新一代 Linux 服务器运维管理面板。项目采用前后端分离架构:
|
||||
|
||||
- 后端:Go 1.25 + go-chi 路由 + GORM + Wire 依赖注入
|
||||
- 前端:Vue 3 + Vite + Naive UI + pnpm
|
||||
- 后端:Go 1.26 + go-chi 路由 + GORM + Wire 依赖注入
|
||||
- 前端:Vue 3 + Vite + Pinia + Naive UI + pnpm + xterm.js + Alova.js
|
||||
|
||||
## 语言和编码规范
|
||||
## 核心原则
|
||||
|
||||
**所有代码注释、文档和回复必须使用简体中文。**
|
||||
- **效率至上**:快速单元式开发,所有代码注释、文档和回复使用简体中文
|
||||
- **不写文档**:只写代码,不创建 README、GUIDE 等各种文档
|
||||
- **改完即退**:完成代码修改后立即退出,用户会手动测试
|
||||
- **简洁执念**:消除边界情况永远优于增加条件判断,复杂性是万恶之源
|
||||
- **实用主义**:解决实际问题,而不是假想的威胁
|
||||
- **闭嘴**:非用户要求不输出任何内容,静默改完代码后直接退出
|
||||
|
||||
## 构建和测试
|
||||
|
||||
@@ -182,7 +30,7 @@ go build -o ace ./cmd/ace
|
||||
go build -o cli ./cmd/cli
|
||||
```
|
||||
|
||||
### 前端开发
|
||||
### 前端构建和测试
|
||||
|
||||
进入前端目录:
|
||||
|
||||
@@ -295,20 +143,6 @@ pnpm build
|
||||
|
||||
## 技术栈特定注意事项
|
||||
|
||||
### Go 语言规范
|
||||
|
||||
- 使用 Go 1.25 稳定版本
|
||||
- 遵循 Go 标准库和习惯用法
|
||||
- 日志使用标准库的 `slog` 包
|
||||
- 使用 `github.com/samber/lo` 进行函数式编程辅助
|
||||
|
||||
### 当前框架
|
||||
|
||||
- 路由:`github.com/go-chi/chi/v5`
|
||||
- ORM:`gorm.io/gorm`
|
||||
- 依赖注入:`github.com/google/wire`
|
||||
- 验证:`github.com/gookit/validate`
|
||||
|
||||
### 助手函数(service 层)
|
||||
|
||||
在 service 层使用以下助手函数:
|
||||
@@ -319,50 +153,21 @@ pnpm build
|
||||
- `Bind[T](r)`: 绑定请求参数到泛型类型 T
|
||||
- `Paginate[T](...)`: 构建分页响应
|
||||
|
||||
### 数据库
|
||||
|
||||
- 使用 SQLite(`github.com/ncruces/go-sqlite3`)
|
||||
- 使用 GORM 进行数据库迁移和操作
|
||||
|
||||
### 安全性
|
||||
|
||||
- 不需要实现命令注入过滤,文件名过滤等,因为这是服务器面板,所有登录的用户都被视为管理员
|
||||
|
||||
## 代码风格
|
||||
|
||||
- 所有代码注释必须使用简体中文
|
||||
- 遵循 Go 官方代码风格
|
||||
- 使用 `gofmt` 格式化代码
|
||||
- 复杂逻辑添加注释说明
|
||||
- 导出的函数和类型必须有注释
|
||||
- service 层以及 data 层方法对外返回的字符串需要使用 gotext 进行国际化处理
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
- 复杂逻辑添加注释说明,简单逻辑不添加注释
|
||||
- 使用 `github.com/samber/lo` 进行函数式编程辅助
|
||||
- 后端对外返回的字符串需要尽可能使用 gotext 进行翻译处理
|
||||
- 前端使用 gettext 进行国际化处理,所有用户可见的字符串都必须使用 gettext 包裹以支持翻译
|
||||
- 禁止手动编辑前后端的翻译文件,项目由外部 Crowdin 自动化管理
|
||||
- 前端 HTTP 请求使用 Alova.js 的 useRequest 等助手函数,无需添加 onError 错误处理
|
||||
- 后端使用 Wire 依赖注入,添加新依赖时需要 `go generate ./...` 重新生成代码
|
||||
- 无需关注命令注入,SQL注入,文件上传等安全问题(这是服务器面板,所有登录的用户都被视为管理员)
|
||||
|
||||
## Wire 依赖注入
|
||||
|
||||
项目使用 Wire 进行依赖注入。当添加新的依赖时:
|
||||
|
||||
1. 在 `cmd/ace/wire.go` 或 `cmd/cli/wire.go` 中添加 provider
|
||||
2. 运行生成命令:
|
||||
|
||||
```bash
|
||||
go generate ./...
|
||||
```
|
||||
|
||||
## 前端开发注意事项
|
||||
|
||||
- 使用 Vue 3 Composition API
|
||||
- UI 框架:Naive UI
|
||||
- 状态管理:Pinia
|
||||
- HTTP 请求:Alova.js,使用 useRequest 等助手函数进行数据获取,无需处理 onError 错误。
|
||||
- 图标:@iconify/vue
|
||||
- 终端:xterm.js
|
||||
- 遵循项目已有的组件结构和编码风格
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
|
||||
## 配置文件
|
||||
|
||||
开发时需要准备配置文件:
|
||||
后端开发配置:
|
||||
|
||||
```bash
|
||||
cp config.example.yml config.yml
|
||||
@@ -378,7 +183,7 @@ cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
|
||||
## 工具使用
|
||||
|
||||
### 文档工具
|
||||
对于不熟悉的库或功能,必须在修改代码前先使用以下工具进行调研:
|
||||
|
||||
1. **查看官方文档**
|
||||
- `resolve-library-id` - 解析库名到 Context7 ID
|
||||
|
||||
@@ -82,10 +82,10 @@ bash <(curl -sSLm 10 https://dl.acepanel.net/helper.sh)
|
||||
|
||||
## Star 历史
|
||||
|
||||
<a href="https://star-history.com/#acepanel/panel&Date">
|
||||
<a href="https://www.star-history.com/?repos=acepanel%2Fpanel&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=acepanel/panel&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=acepanel/panel&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=acepanel/panel&type=Date" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=acepanel/panel&type=date&theme=dark&legend=top-left&sealed_token=A7PSvKwCQys65o30bBWFVq0j-xWQ7q0kkPMbHsRTelnAZTLIVnCIIS18tyeO0LeMqQ4fsyoN1oPeM9dPIHpa1x03AV47vz7Q880xdcO7CH-y6qzXLCMO6g" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=acepanel/panel&type=date&legend=top-left&sealed_token=A7PSvKwCQys65o30bBWFVq0j-xWQ7q0kkPMbHsRTelnAZTLIVnCIIS18tyeO0LeMqQ4fsyoN1oPeM9dPIHpa1x03AV47vz7Q880xdcO7CH-y6qzXLCMO6g" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=acepanel/panel&type=date&legend=top-left&sealed_token=A7PSvKwCQys65o30bBWFVq0j-xWQ7q0kkPMbHsRTelnAZTLIVnCIIS18tyeO0LeMqQ4fsyoN1oPeM9dPIHpa1x03AV47vz7Q880xdcO7CH-y6qzXLCMO6g" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
+16
-7
@@ -1,25 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
_ "time/tzdata"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
if os.Geteuid() != 0 {
|
||||
panic("panel must run as root")
|
||||
return errors.New("panel must run as root")
|
||||
}
|
||||
|
||||
debug.SetGCPercent(10)
|
||||
debug.SetMemoryLimit(256 << 20)
|
||||
|
||||
ace, err := initAce()
|
||||
ace, cleanup, err := initAce()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
if err = ace.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ace.Run()
|
||||
}
|
||||
|
||||
+12
-4
@@ -7,15 +7,23 @@ import (
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/bootstrap"
|
||||
"github.com/acepanel/panel/v3/internal/data"
|
||||
"github.com/acepanel/panel/v3/internal/http/middleware"
|
||||
"github.com/acepanel/panel/v3/internal/job"
|
||||
"github.com/acepanel/panel/v3/internal/route"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
)
|
||||
|
||||
// initAce init application.
|
||||
func initAce() (*app.Ace, error) {
|
||||
panic(wire.Build(bootstrap.ProviderSet, middleware.ProviderSet, route.ProviderSet, service.ProviderSet, data.ProviderSet, apps.ProviderSet, job.ProviderSet, app.NewAce))
|
||||
func initAce() (*app.Ace, func(), error) {
|
||||
panic(wire.Build(
|
||||
bootstrap.ProviderSet,
|
||||
apps.ProviderSet,
|
||||
biz.ProviderSet,
|
||||
data.ProviderSet,
|
||||
service.ProviderSet,
|
||||
route.ProviderSet,
|
||||
job.ProviderSet,
|
||||
app.NewAce,
|
||||
))
|
||||
}
|
||||
|
||||
+289
-137
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/v3/internal/apps/opensearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/percona"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pgadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
@@ -38,10 +39,11 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/s3fs"
|
||||
"github.com/acepanel/panel/v3/internal/apps/supervisor"
|
||||
"github.com/acepanel/panel/v3/internal/apps/valkey"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/bootstrap"
|
||||
"github.com/acepanel/panel/v3/internal/data"
|
||||
"github.com/acepanel/panel/v3/internal/http/middleware"
|
||||
"github.com/acepanel/panel/v3/internal/job"
|
||||
"github.com/acepanel/panel/v3/internal/middleware"
|
||||
"github.com/acepanel/panel/v3/internal/route"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/websitestat"
|
||||
@@ -53,172 +55,322 @@ import (
|
||||
|
||||
// Injectors from wire.go:
|
||||
|
||||
// initAce init application.
|
||||
func initAce() (*app.Ace, error) {
|
||||
func initAce() (*app.Ace, func(), error) {
|
||||
config, err := bootstrap.NewConf()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
locale, err := bootstrap.NewT(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
locale := bootstrap.NewT(config)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
db, err := bootstrap.NewDB(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
manager, err := bootstrap.NewSession(config, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logger := bootstrap.NewLog(config)
|
||||
cacheRepo := data.NewCacheRepo(db)
|
||||
taskRunner := bootstrap.NewRunner(db, logger)
|
||||
taskRepo := data.NewTaskRepo(locale, db, logger, taskRunner)
|
||||
appRepo := data.NewAppRepo(locale, config, db, logger, cacheRepo, taskRepo)
|
||||
userTokenRepo := data.NewUserTokenRepo(locale, config, db)
|
||||
middlewares := middleware.NewMiddlewares(config, manager, appRepo, userTokenRepo)
|
||||
userRepo := data.NewUserRepo(locale, db, logger)
|
||||
userService := service.NewUserService(locale, config, manager, userRepo)
|
||||
userPasskeyRepo := data.NewUserPasskeyRepo(db)
|
||||
userPasskeyService := service.NewUserPasskeyService(locale, config, manager, userPasskeyRepo, userRepo)
|
||||
userTokenService := service.NewUserTokenService(locale, userTokenRepo)
|
||||
databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger)
|
||||
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, logger, databaseServerRepo)
|
||||
databaseRepo := data.NewDatabaseRepo(locale, db, logger, databaseServerRepo, databaseUserRepo)
|
||||
settingRepo := data.NewSettingRepo(locale, db, logger, config, taskRepo)
|
||||
certRepo := data.NewCertRepo(locale, db, logger, settingRepo)
|
||||
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
|
||||
websiteRepo := data.NewWebsiteRepo(locale, db, logger, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
|
||||
projectRepo := data.NewProjectRepo(locale, db, logger)
|
||||
environmentRepo := data.NewEnvironmentRepo(locale, config, cacheRepo, taskRepo)
|
||||
cronRepo := data.NewCronRepo(locale, db, logger)
|
||||
backupRepo := data.NewBackupRepo(locale, config, db, logger, settingRepo, websiteRepo)
|
||||
containerRepo := data.NewContainerRepo(settingRepo)
|
||||
homeService := service.NewHomeService(locale, config, taskRepo, websiteRepo, projectRepo, appRepo, environmentRepo, settingRepo, cronRepo, backupRepo, containerRepo)
|
||||
taskService := service.NewTaskService(taskRepo)
|
||||
websiteService := service.NewWebsiteService(websiteRepo, settingRepo)
|
||||
projectService := service.NewProjectService(projectRepo, settingRepo)
|
||||
databaseService := service.NewDatabaseService(databaseRepo)
|
||||
databaseServerService := service.NewDatabaseServerService(databaseServerRepo)
|
||||
databaseRedisRepo := data.NewDatabaseRedisRepo(locale, db, logger)
|
||||
databaseRedisService := service.NewDatabaseRedisService(databaseRedisRepo)
|
||||
databaseElasticsearchRepo := data.NewDatabaseElasticsearchRepo(locale, db, logger)
|
||||
databaseElasticsearchService := service.NewDatabaseElasticsearchService(databaseElasticsearchRepo)
|
||||
databaseUserService := service.NewDatabaseUserService(databaseUserRepo)
|
||||
backupService := service.NewBackupService(locale, backupRepo)
|
||||
backupAccountRepo := data.NewBackupAccountRepo(locale, db, logger, settingRepo)
|
||||
backupStorageService := service.NewBackupStorageService(locale, backupAccountRepo)
|
||||
certService := service.NewCertService(locale, certRepo)
|
||||
certDNSRepo := data.NewCertDNSRepo(db, logger)
|
||||
certDNSService := service.NewCertDNSService(certDNSRepo)
|
||||
certAccountService := service.NewCertAccountService(certAccountRepo)
|
||||
appService := service.NewAppService(locale, appRepo, cacheRepo, settingRepo)
|
||||
environmentService := service.NewEnvironmentService(locale, environmentRepo, taskRepo)
|
||||
environmentGoService := service.NewEnvironmentGoService(locale, environmentRepo)
|
||||
environmentJavaService := service.NewEnvironmentJavaService(locale, environmentRepo)
|
||||
environmentNodejsService := service.NewEnvironmentNodejsService(locale, environmentRepo)
|
||||
environmentPHPService := service.NewEnvironmentPHPService(locale, config, environmentRepo, taskRepo)
|
||||
environmentPythonService := service.NewEnvironmentPythonService(locale, environmentRepo)
|
||||
environmentDotnetService := service.NewEnvironmentDotnetService(locale, environmentRepo)
|
||||
cronService := service.NewCronService(cronRepo)
|
||||
processService := service.NewProcessService()
|
||||
safeRepo := data.NewSafeRepo(logger)
|
||||
safeService := service.NewSafeService(safeRepo)
|
||||
firewallService := service.NewFirewallService()
|
||||
scanEventRepo, err := data.NewScanEventRepo(settingRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
firewallScanService := service.NewFirewallScanService(scanEventRepo)
|
||||
sshRepo := data.NewSSHRepo(locale, db, logger)
|
||||
sshService := service.NewSSHService(sshRepo)
|
||||
containerService := service.NewContainerService(containerRepo)
|
||||
containerComposeRepo := data.NewContainerComposeRepo()
|
||||
containerComposeService := service.NewContainerComposeService(containerComposeRepo)
|
||||
containerNetworkRepo := data.NewContainerNetworkRepo(settingRepo)
|
||||
containerNetworkService := service.NewContainerNetworkService(containerNetworkRepo)
|
||||
containerImageRepo := data.NewContainerImageRepo(settingRepo)
|
||||
containerImageService := service.NewContainerImageService(containerImageRepo)
|
||||
containerVolumeRepo := data.NewContainerVolumeRepo(settingRepo)
|
||||
containerVolumeService := service.NewContainerVolumeService(containerVolumeRepo)
|
||||
fileService := service.NewFileService(locale, taskRepo)
|
||||
logRepo := data.NewLogRepo(db)
|
||||
logService := service.NewLogService(locale, logRepo)
|
||||
monitorRepo := data.NewMonitorRepo(db, settingRepo)
|
||||
monitorService := service.NewMonitorService(settingRepo, monitorRepo)
|
||||
settingService := service.NewSettingService(locale, db, settingRepo, certRepo, certAccountRepo)
|
||||
systemctlService := service.NewSystemctlService(locale)
|
||||
toolboxNetworkService := service.NewToolboxNetworkService()
|
||||
toolboxSystemService := service.NewToolboxSystemService(locale)
|
||||
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)
|
||||
toolboxSSHService := service.NewToolboxSSHService(locale)
|
||||
toolboxDiskService := service.NewToolboxDiskService(locale)
|
||||
toolboxLogService := service.NewToolboxLogService(locale, db, containerImageRepo, settingRepo)
|
||||
toolboxMigrationService := service.NewToolboxMigrationService(locale, config, logger, settingRepo, websiteRepo, databaseRepo, databaseServerRepo, databaseUserRepo, projectRepo, appRepo, environmentRepo)
|
||||
webHookRepo := data.NewWebHookRepo(locale, db, logger)
|
||||
webHookService := service.NewWebHookService(webHookRepo)
|
||||
templateRepo := data.NewTemplateRepo(locale, logger, cacheRepo)
|
||||
templateService := service.NewTemplateService(locale, templateRepo, settingRepo)
|
||||
websiteStatRepo, err := data.NewWebsiteStatRepo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aggregator := websitestat.NewAggregator()
|
||||
websiteStatService := service.NewWebsiteStatService(settingRepo, websiteStatRepo, websiteRepo, aggregator)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
clickhouseApp := clickhouse.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
databaseServerRepo := data.NewDatabaseServerRepo(db)
|
||||
settingRepo := data.NewSettingRepo(config, db)
|
||||
clickhouseApp := clickhouse.NewApp(locale, databaseServerRepo, settingRepo)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
dockerApp := docker.NewApp()
|
||||
elasticsearchApp := elasticsearch.NewApp(locale)
|
||||
websiteRepo := data.NewWebsiteRepo(db, locale, settingRepo)
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
frpApp := frp.NewApp()
|
||||
frpApp := frp.NewApp(locale)
|
||||
giteaApp := gitea.NewApp()
|
||||
grafanaApp := grafana.NewApp(locale)
|
||||
kafkaApp := kafka.NewApp(locale)
|
||||
mariadbApp := mariadb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
logger, cleanup, err := bootstrap.NewLogger(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
slogLogger := bootstrap.NewSlog(logger)
|
||||
notifyChannelRepo := data.NewNotifyChannelRepo(db)
|
||||
notifyUsecase := biz.NewNotifyUsecase(locale, slogLogger, notifyChannelRepo, settingRepo)
|
||||
taskRunner := bootstrap.NewRunner(notifyUsecase, db, locale, slogLogger)
|
||||
taskRepo := data.NewTaskRepo(db, locale, slogLogger, taskRunner)
|
||||
mysqlApp := mysql.NewApp(locale, databaseServerRepo, settingRepo, taskRepo)
|
||||
mariadbApp := mariadb.NewApp(mysqlApp)
|
||||
memcachedApp := memcached.NewApp(locale)
|
||||
minioApp := minio.NewApp()
|
||||
mongodbApp := mongodb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
mysqlApp := mysql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
mongodbApp := mongodb.NewApp(locale, databaseServerRepo, settingRepo)
|
||||
nginxApp := nginx.NewApp(locale)
|
||||
openrestyApp := openresty.NewApp(locale)
|
||||
openrestyApp := openresty.NewApp(nginxApp)
|
||||
opensearchApp := opensearch.NewApp(locale)
|
||||
perconaApp := percona.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
phpmyadminApp := phpmyadmin.NewApp(locale)
|
||||
perconaApp := percona.NewApp(mysqlApp)
|
||||
pgadminApp := pgadmin.NewApp(config, locale, databaseServerRepo)
|
||||
phpmyadminApp := phpmyadmin.NewApp(config, locale, databaseServerRepo)
|
||||
podmanApp := podman.NewApp()
|
||||
postgresqlApp := postgresql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
prometheusApp := prometheus.NewApp(locale, config, taskRepo)
|
||||
postgresqlApp := postgresql.NewApp(locale, config, databaseServerRepo, settingRepo, taskRepo)
|
||||
prometheusApp := prometheus.NewApp(config, locale, taskRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo, taskRepo)
|
||||
rocketmqApp := rocketmq.NewApp(locale)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
valkeyApp := valkey.NewApp(locale, databaseServerRepo)
|
||||
loader := bootstrap.NewLoader(apacheApp, clickhouseApp, codeserverApp, dockerApp, elasticsearchApp, fail2banApp, frpApp, giteaApp, grafanaApp, kafkaApp, mariadbApp, memcachedApp, minioApp, mongodbApp, mysqlApp, nginxApp, openrestyApp, opensearchApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rocketmqApp, rsyncApp, s3fsApp, supervisorApp, valkeyApp)
|
||||
http := route.NewHttp(config, userService, userPasskeyService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseRedisService, databaseElasticsearchService, databaseUserService, backupService, backupStorageService, certService, certDNSService, certAccountService, appService, environmentService, environmentGoService, environmentJavaService, environmentNodejsService, environmentPHPService, environmentPythonService, environmentDotnetService, cronService, processService, safeService, firewallService, firewallScanService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxNetworkService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, toolboxMigrationService, webHookService, templateService, websiteStatService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo, settingRepo, certRepo)
|
||||
ws := route.NewWs(wsService, toolboxMigrationService)
|
||||
mux, err := bootstrap.NewRouter(locale, middlewares, http, ws)
|
||||
valkeyApp := valkey.NewApp(locale, databaseServerRepo, taskRepo)
|
||||
loader := bootstrap.NewLoader(apacheApp, clickhouseApp, codeserverApp, dockerApp, elasticsearchApp, fail2banApp, frpApp, giteaApp, grafanaApp, kafkaApp, mariadbApp, memcachedApp, minioApp, mongodbApp, mysqlApp, nginxApp, openrestyApp, opensearchApp, perconaApp, pgadminApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rocketmqApp, rsyncApp, s3fsApp, supervisorApp, valkeyApp)
|
||||
manager, err := bootstrap.NewSession(config, db, slogLogger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
reloader, err := bootstrap.NewTLSReloader(config)
|
||||
appRepo := data.NewAppRepo(config, db, locale, slogLogger)
|
||||
userTokenRepo := data.NewUserTokenRepo(config, db, locale)
|
||||
middlewares, err := middleware.NewMiddlewares(config, locale, manager, appRepo, userTokenRepo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
server, err := bootstrap.NewHttp(config, mux, reloader)
|
||||
validator := bootstrap.NewValidator(config, db)
|
||||
alertRepo, err := data.NewAlertRepo(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerRepo := data.NewContainerRepo()
|
||||
alertUsecase := biz.NewAlertUsecase(notifyUsecase, loader, locale, slogLogger, alertRepo, appRepo, containerRepo, databaseServerRepo, settingRepo)
|
||||
alertService := service.NewAlertService(alertUsecase)
|
||||
cacheRepo := data.NewCacheRepo(db)
|
||||
appUsecase := biz.NewAppUsecase(locale, appRepo, cacheRepo, taskRepo)
|
||||
cacheUsecase := biz.NewCacheUsecase(cacheRepo)
|
||||
settingUsecase := biz.NewSettingUsecase(locale, slogLogger, settingRepo, taskRepo)
|
||||
appService := service.NewAppService(loader, appUsecase, cacheUsecase, settingUsecase, locale)
|
||||
backupRepo := data.NewBackupRepo(config, db, locale, slogLogger, settingRepo, websiteRepo)
|
||||
backupUsecase := biz.NewBackupUsecase(notifyUsecase, locale, slogLogger, backupRepo)
|
||||
taskUsecase := biz.NewTaskUsecase(taskRepo)
|
||||
backupService := service.NewBackupService(backupUsecase, taskUsecase, locale)
|
||||
backupAccountRepo := data.NewBackupAccountRepo(db)
|
||||
backupAccountUsecase := biz.NewBackupAccountUsecase(locale, slogLogger, backupAccountRepo, settingRepo)
|
||||
backupStorageService := service.NewBackupStorageService(backupAccountUsecase, locale)
|
||||
certRepo := data.NewCertRepo(db, locale, slogLogger)
|
||||
certUsecase := biz.NewCertUsecase(locale, slogLogger, certRepo, settingRepo)
|
||||
certService := service.NewCertService(certUsecase, locale)
|
||||
certAccountRepo := data.NewCertAccountRepo(db, locale, slogLogger)
|
||||
userRepo := data.NewUserRepo(db, locale)
|
||||
certAccountUsecase := biz.NewCertAccountUsecase(locale, slogLogger, certAccountRepo, userRepo)
|
||||
certAccountService := service.NewCertAccountService(certAccountUsecase)
|
||||
certDNSRepo := data.NewCertDNSRepo(db)
|
||||
certDNSUsecase := biz.NewCertDNSUsecase(certDNSRepo, slogLogger)
|
||||
certDNSService := service.NewCertDNSService(certDNSUsecase)
|
||||
containerUsecase := biz.NewContainerUsecase(locale, containerRepo, settingRepo, taskRepo)
|
||||
containerService := service.NewContainerService(containerUsecase)
|
||||
containerComposeRepo := data.NewContainerComposeRepo()
|
||||
containerComposeUsecase := biz.NewContainerComposeUsecase(containerComposeRepo)
|
||||
containerComposeService := service.NewContainerComposeService(containerComposeUsecase)
|
||||
containerImageRepo := data.NewContainerImageRepo()
|
||||
containerImageUsecase := biz.NewContainerImageUsecase(locale, containerImageRepo, settingRepo, taskRepo)
|
||||
containerImageService := service.NewContainerImageService(containerImageUsecase)
|
||||
containerNetworkRepo := data.NewContainerNetworkRepo()
|
||||
containerNetworkUsecase := biz.NewContainerNetworkUsecase(containerNetworkRepo, settingRepo)
|
||||
containerNetworkService := service.NewContainerNetworkService(containerNetworkUsecase)
|
||||
containerVolumeRepo := data.NewContainerVolumeRepo()
|
||||
containerVolumeUsecase := biz.NewContainerVolumeUsecase(containerVolumeRepo, settingRepo)
|
||||
containerVolumeService := service.NewContainerVolumeService(containerVolumeUsecase)
|
||||
cronRepo := data.NewCronRepo(db, locale)
|
||||
cronUsecase := biz.NewCronUsecase(cronRepo, slogLogger)
|
||||
cronService := service.NewCronService(cronUsecase)
|
||||
databaseUserRepo := data.NewDatabaseUserRepo(db)
|
||||
databaseUserUsecase := biz.NewDatabaseUserUsecase(slogLogger, databaseServerRepo, databaseUserRepo)
|
||||
databaseRepo := data.NewDatabaseRepo(db)
|
||||
databaseUsecase := biz.NewDatabaseUsecase(databaseUserUsecase, locale, slogLogger, databaseRepo, databaseServerRepo)
|
||||
databaseService := service.NewDatabaseService(databaseUsecase)
|
||||
databaseElasticsearchRepo := data.NewDatabaseElasticsearchRepo(db, locale, slogLogger)
|
||||
databaseElasticsearchUsecase := biz.NewDatabaseElasticsearchUsecase(databaseElasticsearchRepo)
|
||||
databaseElasticsearchService := service.NewDatabaseElasticsearchService(databaseElasticsearchUsecase)
|
||||
databaseRedisRepo := data.NewDatabaseRedisRepo(db, locale, slogLogger)
|
||||
databaseRedisUsecase := biz.NewDatabaseRedisUsecase(databaseRedisRepo)
|
||||
databaseRedisService := service.NewDatabaseRedisService(databaseRedisUsecase)
|
||||
databaseServerUsecase := biz.NewDatabaseServerUsecase(locale, slogLogger, databaseServerRepo)
|
||||
databaseServerService := service.NewDatabaseServerService(databaseServerUsecase)
|
||||
databaseUserService := service.NewDatabaseUserService(databaseUserUsecase)
|
||||
environmentRepo := data.NewEnvironmentRepo(config, locale)
|
||||
environmentUsecase := biz.NewEnvironmentUsecase(locale, cacheRepo, environmentRepo, taskRepo)
|
||||
environmentService := service.NewEnvironmentService(environmentUsecase, taskUsecase, locale)
|
||||
environmentDotnetService := service.NewEnvironmentDotnetService(environmentUsecase, locale)
|
||||
environmentGoService := service.NewEnvironmentGoService(environmentUsecase, locale)
|
||||
environmentJavaService := service.NewEnvironmentJavaService(environmentUsecase, locale)
|
||||
environmentNodejsService := service.NewEnvironmentNodejsService(environmentUsecase, locale)
|
||||
environmentPHPService := service.NewEnvironmentPHPService(environmentUsecase, taskUsecase, config, locale)
|
||||
environmentPythonService := service.NewEnvironmentPythonService(environmentUsecase, locale)
|
||||
tamperRepo, err := data.NewTamperRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
tamperUsecase := biz.NewTamperUsecase(notifyUsecase, settingUsecase, locale, slogLogger, tamperRepo)
|
||||
fileService := service.NewFileService(containerUsecase, tamperUsecase, taskUsecase, locale)
|
||||
fileShareRepo := data.NewFileShareRepo(db, locale)
|
||||
fileShareUsecase := biz.NewFileShareUsecase(slogLogger, fileShareRepo)
|
||||
fileShareService := service.NewFileShareService(fileShareUsecase, locale)
|
||||
firewallService := service.NewFirewallService(locale)
|
||||
scanEventRepo, err := data.NewScanEventRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
scanEventUsecase := biz.NewScanEventUsecase(scanEventRepo, settingRepo)
|
||||
firewallScanService := service.NewFirewallScanService(scanEventUsecase)
|
||||
projectRepo := data.NewProjectRepo(db, locale)
|
||||
projectUsecase := biz.NewProjectUsecase(locale, slogLogger, projectRepo)
|
||||
websiteStatRepo, err := data.NewWebsiteStatRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteStatUsecase := biz.NewWebsiteStatUsecase(websiteStatRepo)
|
||||
websiteUsecase := biz.NewWebsiteUsecase(certAccountUsecase, certUsecase, databaseUsecase, databaseUserUsecase, tamperUsecase, websiteStatUsecase, locale, slogLogger, databaseServerRepo, websiteRepo)
|
||||
homeService := service.NewHomeService(appUsecase, backupUsecase, containerUsecase, cronUsecase, databaseServerUsecase, environmentUsecase, projectUsecase, settingUsecase, taskUsecase, websiteUsecase, config, locale)
|
||||
logRepo := data.NewLogRepo(db)
|
||||
logUsecase := biz.NewLogUsecase(logRepo)
|
||||
logService := service.NewLogService(logUsecase, locale)
|
||||
monitorRepo, err := data.NewMonitorRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
monitorUsecase := biz.NewMonitorUsecase(monitorRepo, settingRepo)
|
||||
monitorService := service.NewMonitorService(monitorUsecase, settingUsecase)
|
||||
notifyService := service.NewNotifyService(notifyUsecase)
|
||||
processService := service.NewProcessService()
|
||||
projectService := service.NewProjectService(projectUsecase, settingUsecase)
|
||||
safeRepo := data.NewSafeRepo()
|
||||
safeUsecase := biz.NewSafeUsecase(safeRepo, slogLogger)
|
||||
safeService := service.NewSafeService(safeUsecase)
|
||||
settingService := service.NewSettingService(certAccountUsecase, certUsecase, settingUsecase, db, locale)
|
||||
sshRepo := data.NewSSHRepo(db, locale)
|
||||
sshUsecase := biz.NewSSHUsecase(sshRepo, slogLogger)
|
||||
sshService := service.NewSSHService(sshUsecase)
|
||||
systemctlService := service.NewSystemctlService(locale)
|
||||
tamperService := service.NewTamperService(tamperUsecase, locale)
|
||||
taskService := service.NewTaskService(taskUsecase)
|
||||
templateRepo := data.NewTemplateRepo(slogLogger)
|
||||
templateUsecase := biz.NewTemplateUsecase(locale, cacheRepo, templateRepo)
|
||||
templateService := service.NewTemplateService(settingUsecase, templateUsecase, locale)
|
||||
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)
|
||||
toolboxDiskService := service.NewToolboxDiskService(locale)
|
||||
toolboxLogService := service.NewToolboxLogService(containerImageUsecase, settingUsecase, db, locale)
|
||||
migrationSourceRepo := data.NewMigrationSourceRepo(locale)
|
||||
migrationRemoteRepo := data.NewMigrationRemoteRepo(locale)
|
||||
migrationArchiveRepo := data.NewMigrationArchiveRepo()
|
||||
toolboxMigrationUsecase := biz.NewToolboxMigrationUsecase(locale, slogLogger, migrationSourceRepo, migrationRemoteRepo, migrationArchiveRepo, settingUsecase, websiteUsecase, databaseUsecase, databaseServerUsecase, databaseUserUsecase, backupUsecase, projectUsecase, appUsecase, environmentUsecase)
|
||||
toolboxMigrationService := service.NewToolboxMigrationService(toolboxMigrationUsecase, config, locale, slogLogger)
|
||||
toolboxNetworkService := service.NewToolboxNetworkService(locale)
|
||||
toolboxSSHService := service.NewToolboxSSHService(locale)
|
||||
toolboxSystemService := service.NewToolboxSystemService(locale)
|
||||
userUsecase := biz.NewUserUsecase(locale, slogLogger, userRepo)
|
||||
userService := service.NewUserService(notifyUsecase, userUsecase, config, locale, manager)
|
||||
userPasskeyRepo := data.NewUserPasskeyRepo(db)
|
||||
userPasskeyUsecase := biz.NewUserPasskeyUsecase(userPasskeyRepo)
|
||||
userPasskeyService := service.NewUserPasskeyService(notifyUsecase, userPasskeyUsecase, userUsecase, config, locale, manager)
|
||||
userTokenUsecase := biz.NewUserTokenUsecase(userTokenRepo)
|
||||
userTokenService := service.NewUserTokenService(userTokenUsecase, locale)
|
||||
webHookRepo := data.NewWebHookRepo(db, locale)
|
||||
webHookUsecase := biz.NewWebHookUsecase(locale, slogLogger, webHookRepo)
|
||||
webHookService := service.NewWebHookService(webHookUsecase)
|
||||
websiteService := service.NewWebsiteService(settingUsecase, websiteUsecase, locale)
|
||||
aggregator := websitestat.NewAggregator()
|
||||
websiteStatService := service.NewWebsiteStatService(settingUsecase, websiteStatUsecase, websiteUsecase, aggregator)
|
||||
wsService := service.NewWsService(backupUsecase, certUsecase, sshUsecase, settingUsecase, taskUsecase, config, locale, slogLogger)
|
||||
services := &route.Services{
|
||||
Alert: alertService,
|
||||
App: appService,
|
||||
Backup: backupService,
|
||||
BackupStorage: backupStorageService,
|
||||
Cert: certService,
|
||||
CertAccount: certAccountService,
|
||||
CertDNS: certDNSService,
|
||||
Container: containerService,
|
||||
ContainerCompose: containerComposeService,
|
||||
ContainerImage: containerImageService,
|
||||
ContainerNetwork: containerNetworkService,
|
||||
ContainerVolume: containerVolumeService,
|
||||
Cron: cronService,
|
||||
Database: databaseService,
|
||||
DatabaseElasticsearch: databaseElasticsearchService,
|
||||
DatabaseRedis: databaseRedisService,
|
||||
DatabaseServer: databaseServerService,
|
||||
DatabaseUser: databaseUserService,
|
||||
Environment: environmentService,
|
||||
EnvironmentDotnet: environmentDotnetService,
|
||||
EnvironmentGo: environmentGoService,
|
||||
EnvironmentJava: environmentJavaService,
|
||||
EnvironmentNodejs: environmentNodejsService,
|
||||
EnvironmentPHP: environmentPHPService,
|
||||
EnvironmentPython: environmentPythonService,
|
||||
File: fileService,
|
||||
FileShare: fileShareService,
|
||||
Firewall: firewallService,
|
||||
FirewallScan: firewallScanService,
|
||||
Home: homeService,
|
||||
Log: logService,
|
||||
Monitor: monitorService,
|
||||
Notify: notifyService,
|
||||
Process: processService,
|
||||
Project: projectService,
|
||||
Safe: safeService,
|
||||
Setting: settingService,
|
||||
SSH: sshService,
|
||||
Systemctl: systemctlService,
|
||||
Tamper: tamperService,
|
||||
Task: taskService,
|
||||
Template: templateService,
|
||||
ToolboxBenchmark: toolboxBenchmarkService,
|
||||
ToolboxDisk: toolboxDiskService,
|
||||
ToolboxLog: toolboxLogService,
|
||||
ToolboxMigration: toolboxMigrationService,
|
||||
ToolboxNetwork: toolboxNetworkService,
|
||||
ToolboxSSH: toolboxSSHService,
|
||||
ToolboxSystem: toolboxSystemService,
|
||||
User: userService,
|
||||
UserPasskey: userPasskeyService,
|
||||
UserToken: userTokenService,
|
||||
WebHook: webHookService,
|
||||
Website: websiteService,
|
||||
WebsiteStat: websiteStatService,
|
||||
Ws: wsService,
|
||||
}
|
||||
v := route.NewEndpoints(services)
|
||||
mux, err := bootstrap.NewRouter(loader, config, locale, middlewares, validator, v)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
dependencies := &job.Dependencies{
|
||||
Alert: alertUsecase,
|
||||
Backup: backupUsecase,
|
||||
Cache: cacheUsecase,
|
||||
Cert: certUsecase,
|
||||
CertAccount: certAccountUsecase,
|
||||
FileShare: fileShareUsecase,
|
||||
Monitor: monitorUsecase,
|
||||
Notify: notifyUsecase,
|
||||
ScanEvent: scanEventUsecase,
|
||||
Setting: settingUsecase,
|
||||
Tamper: tamperUsecase,
|
||||
Task: taskUsecase,
|
||||
Website: websiteUsecase,
|
||||
WebsiteStat: websiteStatUsecase,
|
||||
Conf: config,
|
||||
DB: db,
|
||||
T: locale,
|
||||
Log: slogLogger,
|
||||
Aggregator: aggregator,
|
||||
}
|
||||
v2 := job.NewJobs(dependencies)
|
||||
cron, err := bootstrap.NewCron(slogLogger, v2)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
jobs := job.NewJobs(config, db, logger, aggregator, settingRepo, certRepo, certAccountRepo, backupRepo, cacheRepo, taskRepo, scanEventRepo, websiteStatRepo)
|
||||
cron, err := bootstrap.NewCron(config, logger, jobs)
|
||||
reloader, err := bootstrap.NewTLSReloader(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
validation := bootstrap.NewValidator(config, db)
|
||||
ace := app.NewAce(config, mux, server, reloader, gormigrate, cron, taskRunner, validation)
|
||||
return ace, nil
|
||||
server := bootstrap.NewHttp(mux, config, reloader)
|
||||
ace := app.NewAce(mux, config, cron, gormigrate, server, reloader, taskRunner)
|
||||
return ace, func() {
|
||||
cleanup()
|
||||
}, nil
|
||||
}
|
||||
|
||||
+22
-11
@@ -1,21 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/gookit/color"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if os.Geteuid() != 0 {
|
||||
panic("panel must run as root")
|
||||
}
|
||||
|
||||
cli, err := initCli()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err = cli.Run(); err != nil {
|
||||
panic(err)
|
||||
if err := run(); err != nil {
|
||||
color.Errorf("|-%v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
if os.Geteuid() != 0 {
|
||||
return errors.New("panel must run as root")
|
||||
}
|
||||
|
||||
cli, cleanup, err := initCli()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
return cli.Run()
|
||||
}
|
||||
|
||||
+11
-5
@@ -6,14 +6,20 @@ import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/bootstrap"
|
||||
"github.com/acepanel/panel/v3/internal/command"
|
||||
"github.com/acepanel/panel/v3/internal/data"
|
||||
"github.com/acepanel/panel/v3/internal/route"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
)
|
||||
|
||||
// initCli init command line.
|
||||
func initCli() (*app.Cli, error) {
|
||||
panic(wire.Build(bootstrap.ProviderSet, route.ProviderSet, service.ProviderSet, data.ProviderSet, apps.ProviderSet, app.NewCli))
|
||||
func initCli() (*app.Cli, func(), error) {
|
||||
panic(wire.Build(
|
||||
bootstrap.ProviderSet,
|
||||
biz.ProviderSet,
|
||||
data.ProviderSet,
|
||||
service.ProviderSet,
|
||||
command.ProviderSet,
|
||||
app.NewCli,
|
||||
))
|
||||
}
|
||||
|
||||
+59
-88
@@ -8,39 +8,10 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/apache"
|
||||
"github.com/acepanel/panel/v3/internal/apps/clickhouse"
|
||||
"github.com/acepanel/panel/v3/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/v3/internal/apps/docker"
|
||||
"github.com/acepanel/panel/v3/internal/apps/elasticsearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/v3/internal/apps/frp"
|
||||
"github.com/acepanel/panel/v3/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/v3/internal/apps/grafana"
|
||||
"github.com/acepanel/panel/v3/internal/apps/kafka"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/v3/internal/apps/minio"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mongodb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/v3/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/v3/internal/apps/opensearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/percona"
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/prometheus"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rocketmq"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rsync"
|
||||
"github.com/acepanel/panel/v3/internal/apps/s3fs"
|
||||
"github.com/acepanel/panel/v3/internal/apps/supervisor"
|
||||
"github.com/acepanel/panel/v3/internal/apps/valkey"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/bootstrap"
|
||||
"github.com/acepanel/panel/v3/internal/command"
|
||||
"github.com/acepanel/panel/v3/internal/data"
|
||||
"github.com/acepanel/panel/v3/internal/route"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
)
|
||||
|
||||
@@ -50,70 +21,70 @@ import (
|
||||
|
||||
// Injectors from wire.go:
|
||||
|
||||
// initCli init command line.
|
||||
func initCli() (*app.Cli, error) {
|
||||
func initCli() (*app.Cli, func(), error) {
|
||||
config, err := bootstrap.NewConf()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
locale, err := bootstrap.NewT(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
locale := bootstrap.NewT(config)
|
||||
db, err := bootstrap.NewDB(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
logger := bootstrap.NewLog(config)
|
||||
logger, cleanup, err := bootstrap.NewLogger(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
slogLogger := bootstrap.NewSlog(logger)
|
||||
appRepo := data.NewAppRepo(config, db, locale, slogLogger)
|
||||
cacheRepo := data.NewCacheRepo(db)
|
||||
taskRunner := bootstrap.NewRunner(db, logger)
|
||||
taskRepo := data.NewTaskRepo(locale, db, logger, taskRunner)
|
||||
appRepo := data.NewAppRepo(locale, config, db, logger, cacheRepo, taskRepo)
|
||||
userRepo := data.NewUserRepo(locale, db, logger)
|
||||
notifyChannelRepo := data.NewNotifyChannelRepo(db)
|
||||
settingRepo := data.NewSettingRepo(config, db)
|
||||
notifyUsecase := biz.NewNotifyUsecase(locale, slogLogger, notifyChannelRepo, settingRepo)
|
||||
taskRunner := bootstrap.NewRunner(notifyUsecase, db, locale, slogLogger)
|
||||
taskRepo := data.NewTaskRepo(db, locale, slogLogger, taskRunner)
|
||||
appUsecase := biz.NewAppUsecase(locale, appRepo, cacheRepo, taskRepo)
|
||||
websiteRepo := data.NewWebsiteRepo(db, locale, settingRepo)
|
||||
backupRepo := data.NewBackupRepo(config, db, locale, slogLogger, settingRepo, websiteRepo)
|
||||
backupUsecase := biz.NewBackupUsecase(notifyUsecase, locale, slogLogger, backupRepo)
|
||||
cacheUsecase := biz.NewCacheUsecase(cacheRepo)
|
||||
certAccountRepo := data.NewCertAccountRepo(db, locale, slogLogger)
|
||||
userRepo := data.NewUserRepo(db, locale)
|
||||
certAccountUsecase := biz.NewCertAccountUsecase(locale, slogLogger, certAccountRepo, userRepo)
|
||||
certRepo := data.NewCertRepo(db, locale, slogLogger)
|
||||
certUsecase := biz.NewCertUsecase(locale, slogLogger, certRepo, settingRepo)
|
||||
cronRepo := data.NewCronRepo(db, locale)
|
||||
cronUsecase := biz.NewCronUsecase(cronRepo, slogLogger)
|
||||
databaseServerRepo := data.NewDatabaseServerRepo(db)
|
||||
databaseServerUsecase := biz.NewDatabaseServerUsecase(locale, slogLogger, databaseServerRepo)
|
||||
settingUsecase := biz.NewSettingUsecase(locale, slogLogger, settingRepo, taskRepo)
|
||||
userPasskeyRepo := data.NewUserPasskeyRepo(db)
|
||||
settingRepo := data.NewSettingRepo(locale, db, logger, config, taskRepo)
|
||||
databaseServerRepo := data.NewDatabaseServerRepo(locale, db, logger)
|
||||
databaseUserRepo := data.NewDatabaseUserRepo(locale, db, logger, databaseServerRepo)
|
||||
databaseRepo := data.NewDatabaseRepo(locale, db, logger, databaseServerRepo, databaseUserRepo)
|
||||
certRepo := data.NewCertRepo(locale, db, logger, settingRepo)
|
||||
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
|
||||
websiteRepo := data.NewWebsiteRepo(locale, db, logger, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
|
||||
backupRepo := data.NewBackupRepo(locale, config, db, logger, settingRepo, websiteRepo)
|
||||
cliService := service.NewCliService(locale, config, db, appRepo, cacheRepo, userRepo, userPasskeyRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo, certRepo, certAccountRepo)
|
||||
cli := route.NewCli(locale, cliService)
|
||||
command := bootstrap.NewCli(locale, cli)
|
||||
userPasskeyUsecase := biz.NewUserPasskeyUsecase(userPasskeyRepo)
|
||||
userUsecase := biz.NewUserUsecase(locale, slogLogger, userRepo)
|
||||
databaseUserRepo := data.NewDatabaseUserRepo(db)
|
||||
databaseUserUsecase := biz.NewDatabaseUserUsecase(slogLogger, databaseServerRepo, databaseUserRepo)
|
||||
databaseRepo := data.NewDatabaseRepo(db)
|
||||
databaseUsecase := biz.NewDatabaseUsecase(databaseUserUsecase, locale, slogLogger, databaseRepo, databaseServerRepo)
|
||||
tamperRepo, err := data.NewTamperRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
tamperUsecase := biz.NewTamperUsecase(notifyUsecase, settingUsecase, locale, slogLogger, tamperRepo)
|
||||
websiteStatRepo, err := data.NewWebsiteStatRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteStatUsecase := biz.NewWebsiteStatUsecase(websiteStatRepo)
|
||||
websiteUsecase := biz.NewWebsiteUsecase(certAccountUsecase, certUsecase, databaseUsecase, databaseUserUsecase, tamperUsecase, websiteStatUsecase, locale, slogLogger, databaseServerRepo, websiteRepo)
|
||||
validator := bootstrap.NewValidator(config, db)
|
||||
cliService := service.NewCliService(appUsecase, backupUsecase, cacheUsecase, certAccountUsecase, certUsecase, cronUsecase, databaseServerUsecase, notifyUsecase, settingUsecase, userPasskeyUsecase, userUsecase, websiteUsecase, config, db, locale, validator)
|
||||
v := command.Commands(locale, cliService)
|
||||
cliCommand := bootstrap.NewCli(locale, v)
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
clickhouseApp := clickhouse.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
dockerApp := docker.NewApp()
|
||||
elasticsearchApp := elasticsearch.NewApp(locale)
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
frpApp := frp.NewApp()
|
||||
giteaApp := gitea.NewApp()
|
||||
grafanaApp := grafana.NewApp(locale)
|
||||
kafkaApp := kafka.NewApp(locale)
|
||||
mariadbApp := mariadb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
memcachedApp := memcached.NewApp(locale)
|
||||
minioApp := minio.NewApp()
|
||||
mongodbApp := mongodb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
mysqlApp := mysql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
nginxApp := nginx.NewApp(locale)
|
||||
openrestyApp := openresty.NewApp(locale)
|
||||
opensearchApp := opensearch.NewApp(locale)
|
||||
perconaApp := percona.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
phpmyadminApp := phpmyadmin.NewApp(locale)
|
||||
podmanApp := podman.NewApp()
|
||||
postgresqlApp := postgresql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
prometheusApp := prometheus.NewApp(locale, config, taskRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo)
|
||||
rocketmqApp := rocketmq.NewApp(locale)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
valkeyApp := valkey.NewApp(locale, databaseServerRepo)
|
||||
loader := bootstrap.NewLoader(apacheApp, clickhouseApp, codeserverApp, dockerApp, elasticsearchApp, fail2banApp, frpApp, giteaApp, grafanaApp, kafkaApp, mariadbApp, memcachedApp, minioApp, mongodbApp, mysqlApp, nginxApp, openrestyApp, opensearchApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rocketmqApp, rsyncApp, s3fsApp, supervisorApp, valkeyApp)
|
||||
appCli := app.NewCli(command, gormigrate, loader)
|
||||
return appCli, nil
|
||||
cli := app.NewCli(cliCommand, gormigrate)
|
||||
return cli, func() {
|
||||
cleanup()
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,137 +1,154 @@
|
||||
module github.com/acepanel/panel/v3
|
||||
|
||||
go 1.26
|
||||
go 1.27.0
|
||||
|
||||
require (
|
||||
github.com/DeRuina/timberjack v1.4.1
|
||||
github.com/andybalholm/brotli v1.2.1
|
||||
github.com/bddjr/hlfhr v1.5.0
|
||||
github.com/andybalholm/brotli v1.2.3
|
||||
github.com/bddjr/hlfhr v1.6.1
|
||||
github.com/beevik/ntp v1.5.0
|
||||
github.com/cilium/ebpf v0.21.0
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/cilium/ebpf v0.22.0
|
||||
github.com/coder/websocket v1.8.15
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/coreos/go-systemd/v22 v22.7.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/dchest/captcha v1.1.0
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/expr-lang/expr v1.17.8
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-chi/chi/v5 v5.2.5
|
||||
github.com/go-chi/httplog/v3 v3.3.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/go-webauthn/webauthn v0.16.2
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/go-chi/chi/v5 v5.3.2
|
||||
github.com/go-chi/httplog/v3 v3.4.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/go-webauthn/webauthn v0.18.0
|
||||
github.com/gomodule/redigo v1.9.3
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/gookit/color v1.6.0
|
||||
github.com/gookit/validate v1.5.7
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/hashicorp/go-version v1.9.0
|
||||
github.com/klauspost/compress v1.19.2
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/libdns/alidns v1.0.6
|
||||
github.com/libdns/alidns v1.0.7
|
||||
github.com/libdns/cloudflare v0.2.2
|
||||
github.com/libdns/cloudns v1.1.0
|
||||
github.com/libdns/cloudns v1.2.0
|
||||
github.com/libdns/gcore v0.0.0-20250427050847-9964da923833
|
||||
github.com/libdns/huaweicloud v1.0.1
|
||||
github.com/libdns/libdns v1.1.1
|
||||
github.com/libdns/namesilo v1.0.0
|
||||
github.com/libdns/namesilo v1.1.1
|
||||
github.com/libdns/porkbun v1.1.0
|
||||
github.com/libdns/tencentcloud v1.4.3
|
||||
github.com/libdns/westcn v1.0.2
|
||||
github.com/libtnb/cache v1.2.0
|
||||
github.com/libtnb/chix v1.3.2
|
||||
github.com/libtnb/gormstore v1.2.0
|
||||
github.com/libtnb/sessions v1.4.0
|
||||
github.com/libtnb/sqlite v1.0.1
|
||||
github.com/libtnb/utils v1.2.1
|
||||
github.com/medama-io/go-useragent v1.2.3
|
||||
github.com/libtnb/chix/v2 v2.1.1
|
||||
github.com/libtnb/cron v0.5.4
|
||||
github.com/libtnb/gormstore v1.3.0
|
||||
github.com/libtnb/logrotate v0.1.4
|
||||
github.com/libtnb/sessions v1.5.0
|
||||
github.com/libtnb/sqlite v1.2.2
|
||||
github.com/libtnb/utils v1.2.2
|
||||
github.com/libtnb/validator v0.4.3
|
||||
github.com/libtnb/validator/contrib/openapi v0.2.2
|
||||
github.com/medama-io/go-useragent v1.2.4
|
||||
github.com/mholt/acmez/v3 v3.1.6
|
||||
github.com/moby/moby/api v1.54.1
|
||||
github.com/moby/moby/client v0.4.0
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.1
|
||||
github.com/orandin/slog-gorm v1.4.0
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/pelletier/go-toml/v2 v2.4.3
|
||||
github.com/pkg/sftp v1.13.11
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/rhnvrm/simples3 v0.11.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samber/lo v1.53.0
|
||||
github.com/sethvargo/go-limiter v1.1.0
|
||||
github.com/shirou/gopsutil/v4 v4.26.3
|
||||
github.com/sethvargo/go-limiter v1.2.0
|
||||
github.com/shirou/gopsutil/v4 v4.26.7
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/studio-b12/gowebdav v0.12.0
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/studio-b12/gowebdav v0.13.0
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3
|
||||
github.com/urfave/cli/v3 v3.8.0
|
||||
github.com/urfave/cli/v3 v3.11.0
|
||||
github.com/valyala/fastjson v1.6.10
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/net v0.52.0
|
||||
gorm.io/gorm v1.31.1
|
||||
modernc.org/sqlite v1.48.1
|
||||
resty.dev/v3 v3.0.0-beta.6
|
||||
github.com/wneessen/go-mail v0.8.1
|
||||
github.com/xuri/excelize/v2 v2.11.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
golang.org/x/crypto v0.55.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/term v0.45.0
|
||||
gorm.io/gorm v1.31.2
|
||||
modernc.org/sqlite v1.57.0
|
||||
resty.dev/v3 v3.0.0-rc.3
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.1 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/bddjr/shuttingdown v0.1.0 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/boyter/go-string v1.0.5 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/ebitengine/purego v0.10.2 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.3 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/go-webauthn/x v0.2.2 // indirect
|
||||
github.com/gofiber/schema v1.6.0 // indirect
|
||||
github.com/go-webauthn/x v0.3.0 // indirect
|
||||
github.com/gofiber/schema v1.8.2 // indirect
|
||||
github.com/gofiber/utils/v2 v2.2.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/google/subcommands v1.2.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gookit/filter v1.2.3 // indirect
|
||||
github.com/gookit/goutil v0.7.4 // indirect
|
||||
github.com/imega/luaformatter v0.0.0-20211025140405-86b0a68d6bef // indirect
|
||||
github.com/jaevor/go-nanoid v1.4.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.18.4 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/libtnb/securecookie v1.2.0 // indirect
|
||||
github.com/libtnb/securecookie v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.7 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
||||
github.com/timtadh/data-structures v0.6.2 // indirect
|
||||
github.com/timtadh/lexmachine v0.2.3 // indirect
|
||||
github.com/tinylib/msgp v1.6.3 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
modernc.org/libc v1.70.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect
|
||||
golang.org/x/mod v0.38.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/tools v0.48.0 // indirect
|
||||
modernc.org/libc v1.74.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
replace (
|
||||
github.com/mholt/acmez/v3 => github.com/libtnb/acmez/v3 v3.0.0-20260211214515-0d13ab154236
|
||||
github.com/moby/moby/client => github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b
|
||||
github.com/rhnvrm/simples3 => github.com/devhaozi/simples3 v0.0.0-20260124160558-447c94ecedff
|
||||
github.com/stretchr/testify => github.com/libtnb/testify v0.0.0-20260103194301-c7a63ea79696
|
||||
github.com/mholt/acmez/v3 => github.com/libtnb/acmez/v3 v3.0.0-20260406165834-a264acd02292
|
||||
github.com/stretchr/testify => github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7
|
||||
)
|
||||
|
||||
tool github.com/google/wire
|
||||
tool github.com/google/wire/cmd/wire
|
||||
|
||||
@@ -10,13 +10,13 @@ cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
||||
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DeRuina/timberjack v1.4.1 h1:JftM5HN/ITKehAXjtdbGqN5XZIS1biHm7VSjU0Qbtqg=
|
||||
github.com/DeRuina/timberjack v1.4.1/go.mod h1:RLoeQrwrCGIEF8gO5nV5b/gMD0QIy7bzQhBUgpp1EqE=
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3 h1:McILJSbJ5nOcT0MI0aBYhEuufCF329YbqKwFIN0RjCI=
|
||||
github.com/G-Core/gcore-dns-sdk-go v0.3.3/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
@@ -24,13 +24,15 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ=
|
||||
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/bddjr/hlfhr v1.5.0 h1:UJYQBlfNoBDFmG2mRBMexvG3OD73VudDwwwFGKo8h9A=
|
||||
github.com/bddjr/hlfhr v1.5.0/go.mod h1:YmFjslYd0ulNO0CtykhkhRUXJpfdq9sPVHQ4G67D610=
|
||||
github.com/bddjr/hlfhr v1.6.1 h1:bzl0zsL2pl7Gvlbx6l8jZjjv9GbBTeOaX4/+RaXyNXc=
|
||||
github.com/bddjr/hlfhr v1.6.1/go.mod h1:cHRve//dit7d8ZCPUUFZAYW5WCm3LdMoiqX9i9GJdms=
|
||||
github.com/bddjr/shuttingdown v0.1.0 h1:1thUjnzTXNbzexEFkh638ebjln8F4KlqXfLIlBXfdbs=
|
||||
github.com/bddjr/shuttingdown v0.1.0/go.mod h1:+vG4Zp8uhFLT40DBABiA/XxJDHbzybe8RR72i29DFI4=
|
||||
github.com/beevik/ntp v1.5.0 h1:y+uj/JjNwlY2JahivxYvtmv4ehfi3h74fAuABB9ZSM4=
|
||||
github.com/beevik/ntp v1.5.0/go.mod h1:mJEhBrwT76w9D+IfOEGvuzyuudiW9E52U2BaTrMOYow=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -43,11 +45,11 @@ github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl
|
||||
github.com/boyter/go-string v1.0.5 h1:/xcOlWdgelLYLVkUU0xBLfioGjZ9KIMUMI/RXG138YY=
|
||||
github.com/boyter/go-string v1.0.5/go.mod h1:Mww9cDld2S2cdJ0tQffBhsZFMQRA2OJdcjWYZXvZ4Ss=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k=
|
||||
github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ=
|
||||
github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY=
|
||||
github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
@@ -65,61 +67,68 @@ github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfv
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/captcha v1.1.0 h1:2kt47EoYUUkaISobUdTbqwx55xvKOJxyScVfw25xzhQ=
|
||||
github.com/dchest/captcha v1.1.0/go.mod h1:7zoElIawLp7GUMLcj54K9kbw+jEyvz2K0FDdRRYhvWo=
|
||||
github.com/devhaozi/simples3 v0.0.0-20260124160558-447c94ecedff h1:R8fAbRKWR+NVWIkjdK45CXS2cZzXQn8QuoM0Dij2Jso=
|
||||
github.com/devhaozi/simples3 v0.0.0-20260124160558-447c94ecedff/go.mod h1:c2xW30bukipkBlWNnXG1wDjq3gykQ6ww2AB/9NHMLMY=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
||||
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
|
||||
github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.3 h1:oQBnFATpNdY8gJHTndDDv5Xl4QqNaz51G5LLEPhng3Q=
|
||||
github.com/fxamacker/cbor/v2 v2.9.3/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||
github.com/go-chi/httplog/v3 v3.3.0 h1:Gr6Y7nSzbpyCyRwKPOVKjDH3BH6TH5uvRNDsTZWDpvU=
|
||||
github.com/go-chi/httplog/v3 v3.3.0/go.mod h1:N/J1l5l1fozUrqIVuT8Z/HzNeSy8TF2EFyokPLe6y2w=
|
||||
github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
|
||||
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||
github.com/go-chi/httplog/v3 v3.4.0 h1:gO4fvt8HEtFwHq926HoKe1aV2DymfPJuZy4+U4zwT3I=
|
||||
github.com/go-chi/httplog/v3 v3.4.0/go.mod h1:tDhJo9G+F4mioDgX4pKbyA0uVZwCtHejoSsDkvJkFkU=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5 h1:1OyorA5LtdQw12cyJDEHuTrEV3GiXiIhS4/QTTa/SM8=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5/go.mod h1:mj9ekk/7CPF3VjopaFvWKN2v7fN3D9d3eEOAXRhi/+M=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.6/go.mod h1:PZpedQc4tWaxn6kvXicwhinh3L0seLpMc5ReKRX5id4=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
|
||||
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-webauthn/webauthn v0.16.2 h1:n116UuvIa7nUVGFP2hO9U24gBqhJTcmbU3ph0wgVzFM=
|
||||
github.com/go-webauthn/webauthn v0.16.2/go.mod h1:R2xjJxSPat5PYKg5r6cUmqXgbHtbv4GmF6uGkqFMLNI=
|
||||
github.com/go-webauthn/x v0.2.2 h1:zIiipvMbr48CXi5RG0XdBJR94kd8I5LfzHPb/q+YYmk=
|
||||
github.com/go-webauthn/x v0.2.2/go.mod h1:IpJ5qyWB9NRhLX3C7gIfjTU7RZLXEP6kzFkoVSE7Fz4=
|
||||
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
|
||||
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
|
||||
github.com/go-webauthn/webauthn v0.18.0 h1:PC8R3PNLEmjZf++WwcQlo1Z39S9rf8ma69rlwkypZhA=
|
||||
github.com/go-webauthn/webauthn v0.18.0/go.mod h1:ymzZQhx3D/PrDjznemBdQJ23gHTaSDxUchM7sH1lUCg=
|
||||
github.com/go-webauthn/x v0.3.0 h1:Q2X9vbrlP0Ed+QGEzixh1hthGZlDnzVT0XH/9IIQ0kE=
|
||||
github.com/go-webauthn/x v0.3.0/go.mod h1:5OkdSQdOy7taRXWqvNHggtaPffmW94ybu3rZEER4I+I=
|
||||
github.com/gofiber/schema v1.8.2 h1:wq+LO2xEGlsqma/8Akp9PUebQ6vcsYmF0xYQ4F2ijvU=
|
||||
github.com/gofiber/schema v1.8.2/go.mod h1:iyAMJztdyky7Pk2U7bwUP3EHjzMqUNjZ/hglE0nYO/g=
|
||||
github.com/gofiber/utils/v2 v2.2.0 h1:YSSmCzQponq/f9uSOg2HtXC5qK1Dmor0o6DqaQVz8GE=
|
||||
github.com/gofiber/utils/v2 v2.2.0/go.mod h1:Ieopk6sQh7rbhQ12aBNCJtJuG0gxAg0nz63sFCrrOmE=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
@@ -144,13 +153,11 @@ github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
|
||||
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
@@ -162,14 +169,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=
|
||||
github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=
|
||||
github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=
|
||||
github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gookit/filter v1.2.3 h1:Zo7cBOtsVzAoa/jtf+Ury6zlsbJXqInFdUpbbnB2vMM=
|
||||
github.com/gookit/filter v1.2.3/go.mod h1:nFLJcOV8dRgS1iiX23gUQgmHUhpuS40qCvAGgIvA1pM=
|
||||
github.com/gookit/goutil v0.7.4 h1:OWgUngToNz+bPlX5aP+EMG31DraEU63uvKMwwT3vseM=
|
||||
github.com/gookit/goutil v0.7.4/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/gookit/validate v1.5.7 h1:W0nqxAbgCJ4ND/oR+Tx8yyYn6l+PkFVvGHlG7LxpYsc=
|
||||
github.com/gookit/validate v1.5.7/go.mod h1:VeThM1saXWVJIG/Yn627PJL9yvap0s4bsjPUIV8I0UI=
|
||||
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
@@ -222,8 +223,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
@@ -237,63 +238,75 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
|
||||
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
|
||||
github.com/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
|
||||
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
|
||||
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
|
||||
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/libdns/alidns v1.0.6 h1:/Ii428ty6WHFJmE24rZxq2taq++gh7rf9jhgLfp8PmM=
|
||||
github.com/libdns/alidns v1.0.6/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
|
||||
github.com/libdns/alidns v1.0.7 h1:0BhTUFlWIUyb5U7wujGqsyoeZkleUR/hMCtdZDS8Oc8=
|
||||
github.com/libdns/alidns v1.0.7/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
|
||||
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
|
||||
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
|
||||
github.com/libdns/cloudns v1.1.0 h1:W+1MadtxKySn3b5RITFTsXgTIvr5VoO5x97cewjlDcs=
|
||||
github.com/libdns/cloudns v1.1.0/go.mod h1:/22V6tYYDALDpM4pw/RGGJ+X2F1Luibty9kKpKvkqBM=
|
||||
github.com/libdns/cloudns v1.2.0 h1:V299bWj6HHTsXSzoEkRsKNBb9V/HGj3zp4OYzJ31JX8=
|
||||
github.com/libdns/cloudns v1.2.0/go.mod h1:zKx/cXy9W3f4bGjbEtOx+B435flBrz1uibNr3h27+aU=
|
||||
github.com/libdns/gcore v0.0.0-20250427050847-9964da923833 h1:/gsawtsq03cI7qgK65v16tYHIh40omL9YEzHWqnc63I=
|
||||
github.com/libdns/gcore v0.0.0-20250427050847-9964da923833/go.mod h1:jZJEV7pCTOJFlaUhHty+YwR05dzoSmInXK/vT3wOeVg=
|
||||
github.com/libdns/huaweicloud v1.0.1 h1:TdfpPY//3/QPhEGCyvR8u2DTlGxzpyaIsqofH5kYFyA=
|
||||
github.com/libdns/huaweicloud v1.0.1/go.mod h1:lMHNSEqLZeuE4N28ytaqLZia3NPuaaz1ldAH/73bOQE=
|
||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/libdns/namesilo v1.0.0 h1:Shwbj9YnSp4NR617sBOSfDkFozEnWInWCxNwrDvPkCg=
|
||||
github.com/libdns/namesilo v1.0.0/go.mod h1:qdojVsogA6eZDjDPdpIUIfv4ymiX+FuqcVV1eNmHDss=
|
||||
github.com/libdns/namesilo v1.1.1 h1:YNVt3LeoqOlU7qhD09t4FhfMsPC9WrRWmCG/hOaUTnM=
|
||||
github.com/libdns/namesilo v1.1.1/go.mod h1:ndr1T+DizW+go/96J2cMhO9eFHQQNp70gZZQAK6bcGA=
|
||||
github.com/libdns/porkbun v1.1.0 h1:X763NqXjW26VEl7GvBtF/3CGeuGt9JqoQ35mwIlx40E=
|
||||
github.com/libdns/porkbun v1.1.0/go.mod h1:JL6NfXkkSlLr24AI5Fv0t3/Oa6PXOSOerVsOmr8+URs=
|
||||
github.com/libdns/tencentcloud v1.4.3 h1:xJHYLL1TdPeOtUr6Bu6dHTd1TU6/VFm7BFc2EAzAlvc=
|
||||
github.com/libdns/tencentcloud v1.4.3/go.mod h1:Be9gY3tDa12DuAPU79RV9NZIcjY6qg5s7zKPsP26yAM=
|
||||
github.com/libdns/westcn v1.0.2 h1:PA2M3tME5/0T3klPMzSHvGk1EWnGNNkJiKxaJjFalnM=
|
||||
github.com/libdns/westcn v1.0.2/go.mod h1:iKpk8jjOU+793Yp8nHoihPTCR6N1KWtm4/r8BB9mVnk=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260211214515-0d13ab154236 h1:Lna+MN0rG1u3kQjE3SiIEcI1cYZED76EjP0X2TekBMg=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260211214515-0d13ab154236/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/libtnb/cache v1.2.0 h1:6MHHRTzP2GgNs1gD9VJoTIeQ3ud20tkkgvbOtzIf0Zs=
|
||||
github.com/libtnb/cache v1.2.0/go.mod h1:QzCzSS5KBP2dcwMZtwKZO94eFMOViYMkUuxeLallZTE=
|
||||
github.com/libtnb/chix v1.3.2 h1:LkA/+OaHeMyTiQoWEcCQKAMSKDZ0LxBXSENQO5d+OKk=
|
||||
github.com/libtnb/chix v1.3.2/go.mod h1:ItssStBa/ov7v4qujyYzORqIf7Z6DO7Q4carOM7pyAQ=
|
||||
github.com/libtnb/gormstore v1.2.0 h1:VdNTiG8Ah+glDFNxtj+ZrBhcqiLFxMviqlz2gez6zWU=
|
||||
github.com/libtnb/gormstore v1.2.0/go.mod h1:8A5QzeZxi1MpSmjUVsHTDAL6KnU84feIXMutFLPawwA=
|
||||
github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b h1:73OlJdl4Zk6Ipnm7KKeE6yCjfd7nk0BhTArujx6WJCU=
|
||||
github.com/libtnb/moby/client v0.0.0-20260127104057-d4b7902a560b/go.mod h1:bBQRIiaGR9EZBC+x5hnwunNgcDOts7E1Oa7AowJwMFU=
|
||||
github.com/libtnb/securecookie v1.2.0 h1:2uc0PBDm0foeSTrcZ9QTX1IEjf6kFEwfgEYSIXQSKrA=
|
||||
github.com/libtnb/securecookie v1.2.0/go.mod h1:ja+wNGnQzYqcqXQnJWu6icsaWi5JEBwNEMJ2ReTVDxA=
|
||||
github.com/libtnb/sessions v1.4.0 h1:oQn4BckstpvqujSXejUcGAF4SQwTKFPIBe/Hv7s0aOs=
|
||||
github.com/libtnb/sessions v1.4.0/go.mod h1:PvxiaI3PCD+kzjPmIrpV3s9e4ACps6OUaXgmKCYlUFo=
|
||||
github.com/libtnb/sqlite v1.0.1 h1:H4kdU3LBOjdxfQy8s02/wG1rNmp8GMiqzgel0axewtI=
|
||||
github.com/libtnb/sqlite v1.0.1/go.mod h1:zuagCP0nvIJwr4e0kdzq9Yc2Pw5HlVOoLT6ZmzmjOes=
|
||||
github.com/libtnb/testify v0.0.0-20260103194301-c7a63ea79696 h1:GN0Y3DG27mMruX536k0jtCSLegjJb/gypzj5gZL6tRI=
|
||||
github.com/libtnb/testify v0.0.0-20260103194301-c7a63ea79696/go.mod h1:HeQeTfKU6tj2Lx1z79UacwYeDioo6M4ZD7BDDI6+rrg=
|
||||
github.com/libtnb/utils v1.2.1 h1:LJmReRREnpqfHyy9PZtNgBh3ZaIGct81b8ZaAsolMkM=
|
||||
github.com/libtnb/utils v1.2.1/go.mod h1:o6LEDeC42PXI21uLWdWJWTVYvR9BtAZfzzTGJVQoQiU=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260406165834-a264acd02292 h1:FF1ZlwFFjpwT6CtBodr/NV4OMJPMC7plTmfzqhXrlSk=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260406165834-a264acd02292/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
||||
github.com/libtnb/chix/v2 v2.1.1 h1:QQwHXUyPaUUp/KOwzXf2NB0IRKb9v2pqIdOIItzBPiE=
|
||||
github.com/libtnb/chix/v2 v2.1.1/go.mod h1:OEY9v0976U/IJzP6n8VdhgXH17VL2muo3EQWsIUyxm8=
|
||||
github.com/libtnb/cron v0.5.4 h1:sRJL2Xb2X+l1UvgGj0cxGyCXrGVXQG0rCFIPsIYB1RE=
|
||||
github.com/libtnb/cron v0.5.4/go.mod h1:hVcFjf3S5YxxeCg4M/np9Eh3p6EU4xgo2KodRd9YA5I=
|
||||
github.com/libtnb/gormstore v1.3.0 h1:OvVTYHZ9MCHI3Anf2ITtJLunAefyBQWG96EmHCE0eLM=
|
||||
github.com/libtnb/gormstore v1.3.0/go.mod h1:Z/n6VlVdLPmvunSM8sYGNHVebTTN5yUSTZsyNxW2IZc=
|
||||
github.com/libtnb/logrotate v0.1.4 h1:ZSv5LbMjqJA/oLLbRS2ZRVzXGFU7Q+zqkNjTz8vZfI0=
|
||||
github.com/libtnb/logrotate v0.1.4/go.mod h1:0IYk9Olsc8C59nsUm05DUvJBfG0YSSHtEgiAj470Rlw=
|
||||
github.com/libtnb/securecookie v1.4.0 h1:SkKHO7T5I4aRGV7/6fnYYsleQDnnDzeAmTDA0GMPD98=
|
||||
github.com/libtnb/securecookie v1.4.0/go.mod h1:mg1i9HfstsYBGwCfQdU+3Z1GuieyZRAxbkFUnrzchJU=
|
||||
github.com/libtnb/sessions v1.5.0 h1:gvTRkSR8lpp1PB0Nms/kc+gh1BwJ411fxLjnZLYItLs=
|
||||
github.com/libtnb/sessions v1.5.0/go.mod h1:/Q/+lO7DcnJJEU1nL59ScPV/V8ct34F8BjoEMwwxjRw=
|
||||
github.com/libtnb/sqlite v1.2.2 h1:Ku5hAPP5B3A4kQcDn4Z3qyNMafKzuAbnFZX4oD9wR4I=
|
||||
github.com/libtnb/sqlite v1.2.2/go.mod h1:JkAuxM7HHo0tc7dQENjIGpp/yIfrLX5nKr5ME9wuvmI=
|
||||
github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7 h1:ZzONsNhw69uC/wJQlMj66n1zfsRgKAElfJ//E9G380U=
|
||||
github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7/go.mod h1:HeQeTfKU6tj2Lx1z79UacwYeDioo6M4ZD7BDDI6+rrg=
|
||||
github.com/libtnb/utils v1.2.2 h1:HUqgAIb+TIrZPXRytYB5XqzkEXG2U4a4/ZGpprNtL5A=
|
||||
github.com/libtnb/utils v1.2.2/go.mod h1:KUCx2+Phw3cLXOwuQDy6XwFOliaYuU1rpAuJQuHf+lQ=
|
||||
github.com/libtnb/validator v0.4.3 h1:tq3nOOt9YIcrWgXmM9BQAr3ofjiUBAyeUDdHHlpip+o=
|
||||
github.com/libtnb/validator v0.4.3/go.mod h1:yssIkchUNzAQc8EeP99rwIeYbFE2usrYbN1Ki5pamfQ=
|
||||
github.com/libtnb/validator/contrib/openapi v0.2.2 h1:i+6l1ZvHsMQAOjfFgAR7wOHQMxQtORVqIHiYy8gScV4=
|
||||
github.com/libtnb/validator/contrib/openapi v0.2.2/go.mod h1:8qT0y4bs8o400kv1YqD6K+4P04x128yPmnarLwWeHaQ=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/medama-io/go-useragent v1.2.3 h1:jTv5NI+dn2hAe6zlagfXe/Y4934/YPzqxvP/gP0DjCQ=
|
||||
github.com/medama-io/go-useragent v1.2.3/go.mod h1:H9GYWth4IN8vAFZh5LeARza7VwM4jK9uk7Tb9huVzLw=
|
||||
github.com/medama-io/go-useragent v1.2.4 h1:kDux3lCjZvBpccKus/goaZl6YMnrl9D8Tx/wb4HqRMI=
|
||||
github.com/medama-io/go-useragent v1.2.4/go.mod h1:H9GYWth4IN8vAFZh5LeARza7VwM4jK9uk7Tb9huVzLw=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@@ -304,8 +317,10 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
|
||||
github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
|
||||
github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||
github.com/moby/moby/client v0.5.1 h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
|
||||
github.com/moby/moby/client v0.5.1/go.mod h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
@@ -320,13 +335,15 @@ github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsT
|
||||
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
|
||||
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
||||
github.com/pkg/sftp v1.13.11 h1:0N92SLTB8JqASJB14ZLHHzFnBV8mG9zw4K7jghEFWuE=
|
||||
github.com/pkg/sftp v1.13.11/go.mod h1:uNkH9roSXglNJqM+glJJi+TQXQUm0fXFWqCFmT8hsN0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -346,8 +363,10 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0=
|
||||
github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
|
||||
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
@@ -357,10 +376,12 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
|
||||
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/sethvargo/go-limiter v1.1.0 h1:eLeZVQ2zqJOiEs03GguqmBVG6/T6lsZB+6PP1t7J6fA=
|
||||
github.com/sethvargo/go-limiter v1.1.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
|
||||
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/sethvargo/go-limiter v1.2.0 h1:XKL1vsaQ2zztVJrnZSzpRWCq/aLQqMllJ/3D/0bt/cw=
|
||||
github.com/sethvargo/go-limiter v1.2.0/go.mod h1:RC+qY2R7PAK81mBCrZEJlUlKnXSIqqQ8B7G44UgZ/1E=
|
||||
github.com/shamaton/msgpack/v3 v3.2.0 h1:1q2Ms+MWmuRju+PuDMSFDB7p7621npeX4zprJN5Zck8=
|
||||
github.com/shamaton/msgpack/v3 v3.2.0/go.mod h1:sgBYvEiyz8JR1NC3yGRoPVME9xXovpnh3l/plW1nfRo=
|
||||
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
@@ -380,9 +401,11 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM=
|
||||
github.com/studio-b12/gowebdav v0.12.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/studio-b12/gowebdav v0.13.0 h1:OcwSg6IQHOFNdYHn3bPOHwSE8looG8N56Y5xTT1asqQ=
|
||||
github.com/studio-b12/gowebdav v0.13.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||
github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU=
|
||||
github.com/timtadh/data-structures v0.6.1/go.mod h1:uYUnI1cQi/5yMCc7s23I+x8Mn8BCMf4WgK+7/4QSEk4=
|
||||
github.com/timtadh/data-structures v0.6.2 h1:zybDnU5NLjJ7WKMDJpvVwczQuf1wSLBgdRHZ9O4AqJ0=
|
||||
@@ -391,8 +414,8 @@ github.com/timtadh/getopt v1.0.0/go.mod h1:L3EL6YN2G0eIAhYBo9b7SB9d/kEQmdnwthIlM
|
||||
github.com/timtadh/lexmachine v0.2.2/go.mod h1:GBJvD5OAfRn/gnp92zb9KTgHLB7akKyxmVivoYCcjQI=
|
||||
github.com/timtadh/lexmachine v0.2.3 h1:ZqlfHnfMcAygtbNM5Gv7jQf8hmM8LfVzDjfCrq235NQ=
|
||||
github.com/timtadh/lexmachine v0.2.3/go.mod h1:oK1NW+93fQSIF6s+J6sXBFWsCPCFbNmrwKV1i0aqvW0=
|
||||
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
|
||||
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
|
||||
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
@@ -400,15 +423,23 @@ github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZ
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3 h1:ClWHRIz18WRVT3RD9y9uTwEWU9eEi+VYFEIumg05FLk=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20260220081509-8e17ce617db3/go.mod h1:ALbEe81QPWOZjDKCKNWodG2iqCMtregG8+ebQgjx2+4=
|
||||
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
||||
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/urfave/cli/v3 v3.11.0 h1:P/euJp99kb9p0tlVY+iYTLYYTAQlfl0hR2gUO1Img1Q=
|
||||
github.com/urfave/cli/v3 v3.11.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
|
||||
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
|
||||
github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM=
|
||||
github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.11.0 h1:HxaEFl6sRN2+8J5a8HaKq+0M4FsjBGMnWWtjOCPSG88=
|
||||
github.com/xuri/excelize/v2 v2.11.0/go.mod h1:jxFLbzaIwGQ5ufFNvYfUOHqXhfPaNmP14KWfmNz2Uak=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
@@ -416,30 +447,46 @@ github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q=
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
|
||||
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -451,8 +498,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -467,8 +514,8 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -477,8 +524,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -495,17 +542,16 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -525,8 +571,8 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@@ -560,44 +606,46 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo=
|
||||
gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
||||
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
|
||||
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||
modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k=
|
||||
modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA=
|
||||
modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/sqlite v1.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg=
|
||||
modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||
resty.dev/v3 v3.0.0-beta.6 h1:ghRdNpoE8/wBCv+kTKIOauW1aCrSIeTq7GxtfYgtevU=
|
||||
resty.dev/v3 v3.0.0-beta.6/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
|
||||
resty.dev/v3 v3.0.0-rc.3 h1:k24LZ03Cb4Ue5e6O/Pfxu5TQRBBYGES6wm2wceia+Io=
|
||||
resty.dev/v3 v3.0.0-rc.3/go.mod h1:NTOerrC/4T7/FE6tXIZGIysXXBdgNqwMZuKtxpea9NM=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
|
||||
+8
-6
@@ -13,8 +13,7 @@ import (
|
||||
"github.com/bddjr/hlfhr"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/gookit/validate"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/libtnb/cron"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/tlscert"
|
||||
@@ -31,7 +30,7 @@ type Ace struct {
|
||||
runner types.TaskRunner
|
||||
}
|
||||
|
||||
func NewAce(conf *config.Config, router *chi.Mux, server *hlfhr.Server, reloader *tlscert.Reloader, migrator *gormigrate.Gormigrate, cron *cron.Cron, runner types.TaskRunner, _ *validate.Validation) *Ace {
|
||||
func NewAce(router *chi.Mux, conf *config.Config, cron *cron.Cron, migrator *gormigrate.Gormigrate, server *hlfhr.Server, reloader *tlscert.Reloader, runner types.TaskRunner) *Ace {
|
||||
return &Ace{
|
||||
conf: conf,
|
||||
router: router,
|
||||
@@ -51,7 +50,9 @@ func (r *Ace) Run() error {
|
||||
fmt.Println("[DB] database migrated")
|
||||
|
||||
// start cron scheduler
|
||||
r.cron.Start()
|
||||
if err := r.cron.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("[CRON] cron scheduler started")
|
||||
|
||||
// create context for runner
|
||||
@@ -95,8 +96,9 @@ func (r *Ace) Run() error {
|
||||
fmt.Println("[APP] shutting down gracefully...")
|
||||
|
||||
// stop cron scheduler
|
||||
ctx := r.cron.Stop()
|
||||
<-ctx.Done()
|
||||
cronCtx, cronCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
_ = r.cron.Stop(cronCtx)
|
||||
cronCancel()
|
||||
fmt.Println("[CRON] cron scheduler stopped")
|
||||
|
||||
// stop task runner
|
||||
|
||||
+3
-9
@@ -5,10 +5,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/gookit/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/apploader"
|
||||
)
|
||||
|
||||
type Cli struct {
|
||||
@@ -16,7 +13,7 @@ type Cli struct {
|
||||
migrator *gormigrate.Gormigrate
|
||||
}
|
||||
|
||||
func NewCli(cmd *cli.Command, migrator *gormigrate.Gormigrate, _ *apploader.Loader) *Cli {
|
||||
func NewCli(cmd *cli.Command, migrator *gormigrate.Gormigrate) *Cli {
|
||||
IsCli = true
|
||||
return &Cli{
|
||||
cmd: cmd,
|
||||
@@ -29,9 +26,6 @@ func (r *Cli) Run() error {
|
||||
// 这里不处理错误,这么做是为了在异常时用户可以用 fix 命令尝试修复
|
||||
_ = r.migrator.Migrate()
|
||||
|
||||
if err := r.cmd.Run(context.TODO(), os.Args); err != nil {
|
||||
color.Errorf("|-%v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
// 错误必须向上返回,调用方据此以非零码退出,后台任务才能正确判定失败
|
||||
return r.cmd.Run(context.TODO(), os.Args)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HealthLevelError 需要用户立即处理
|
||||
const HealthLevelError = "error"
|
||||
|
||||
// HealthLevelWarning 提示性问题,通常已被系统自动降级处理
|
||||
const HealthLevelWarning = "warning"
|
||||
|
||||
// HealthIssue 单条健康问题
|
||||
// Key 为稳定标识符(如 database:stat),前端据此选择翻译文案
|
||||
// Message 为原始错误详情,供诊断参考
|
||||
type HealthIssue struct {
|
||||
Key string `json:"key"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Since time.Time `json:"since"`
|
||||
}
|
||||
|
||||
type healthRegistry struct {
|
||||
mu sync.RWMutex
|
||||
issues map[string]HealthIssue
|
||||
}
|
||||
|
||||
// Health 全局健康状态注册表,供各后台任务上报/清除故障
|
||||
var Health = &healthRegistry{issues: make(map[string]HealthIssue)}
|
||||
|
||||
// Report 上报或更新一条健康问题
|
||||
// 同 key 重复上报时保留首次上报时间,仅更新 message 和 level
|
||||
func (h *healthRegistry) Report(key, level, message string) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if existing, ok := h.issues[key]; ok {
|
||||
existing.Level = level
|
||||
existing.Message = message
|
||||
h.issues[key] = existing
|
||||
return
|
||||
}
|
||||
h.issues[key] = HealthIssue{
|
||||
Key: key,
|
||||
Level: level,
|
||||
Message: message,
|
||||
Since: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Clear 清除指定 key 的健康问题
|
||||
func (h *healthRegistry) Clear(key string) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
delete(h.issues, key)
|
||||
}
|
||||
|
||||
// Snapshot 返回当前所有健康问题,按 Since 升序(越早发生越靠前)
|
||||
func (h *healthRegistry) Snapshot() []HealthIssue {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
|
||||
result := make([]HealthIssue, 0, len(h.issues))
|
||||
for _, issue := range h.issues {
|
||||
result = append(result, issue)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Since.Before(result[j].Since)
|
||||
})
|
||||
return result
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
@@ -18,6 +19,8 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
var mpmEventRegexp = regexp.MustCompile(`(?s)<IfModule mpm_event_module>(.*?)</IfModule>`)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
@@ -34,10 +37,17 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.SaveConfig)
|
||||
r.Get("/error_log", s.ErrorLog)
|
||||
r.Post("/clear_error_log", s.ClearErrorLog)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("apache")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/apache/conf/httpd.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/apache/conf/httpd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -53,7 +63,7 @@ func (s *App) SaveConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/apache/conf/httpd.conf", app.Root), req.Config, 0600); err != nil {
|
||||
if err = io.Write(app.Root+"/server/apache/conf/httpd.conf", req.Config, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -155,3 +165,83 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Apache 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
defaultConf, err := io.Read(app.Root + "/server/apache/conf/extra/httpd-default.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
mpmConf, _ := io.Read(app.Root + "/server/apache/conf/extra/httpd-mpm.conf")
|
||||
eventBlock := ""
|
||||
if m := mpmEventRegexp.FindStringSubmatch(mpmConf); len(m) > 1 {
|
||||
eventBlock = m[1]
|
||||
}
|
||||
|
||||
// 面板统一写入 httpd-default.conf,未写入过的从 httpd-mpm.conf 的 event 块读取默认值
|
||||
get := func(key string) string {
|
||||
if v := confval.Directive.Get(defaultConf, key); v != "" {
|
||||
return v
|
||||
}
|
||||
return confval.Directive.Get(eventBlock, key)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
// MPM 事件模型
|
||||
StartServers: get("StartServers"),
|
||||
MinSpareThreads: get("MinSpareThreads"),
|
||||
MaxSpareThreads: get("MaxSpareThreads"),
|
||||
ThreadsPerChild: get("ThreadsPerChild"),
|
||||
MaxRequestWorkers: get("MaxRequestWorkers"),
|
||||
MaxConnectionsPerChild: get("MaxConnectionsPerChild"),
|
||||
// 连接设置
|
||||
Timeout: get("Timeout"),
|
||||
KeepAlive: get("KeepAlive"),
|
||||
MaxKeepAliveRequests: get("MaxKeepAliveRequests"),
|
||||
KeepAliveTimeout: get("KeepAliveTimeout"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Apache 配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confPath := app.Root + "/server/apache/conf/extra/httpd-default.conf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// MPM 参数一并写入 httpd-default.conf,其 Include 顺序在 httpd-mpm.conf 之后,顶层定义覆盖块内默认值
|
||||
config = confval.Directive.Set(config, "StartServers", req.StartServers)
|
||||
config = confval.Directive.Set(config, "MinSpareThreads", req.MinSpareThreads)
|
||||
config = confval.Directive.Set(config, "MaxSpareThreads", req.MaxSpareThreads)
|
||||
config = confval.Directive.Set(config, "ThreadsPerChild", req.ThreadsPerChild)
|
||||
config = confval.Directive.Set(config, "MaxRequestWorkers", req.MaxRequestWorkers)
|
||||
config = confval.Directive.Set(config, "MaxConnectionsPerChild", req.MaxConnectionsPerChild)
|
||||
config = confval.Directive.Set(config, "Timeout", req.Timeout)
|
||||
config = confval.Directive.Set(config, "KeepAlive", req.KeepAlive)
|
||||
config = confval.Directive.Set(config, "MaxKeepAliveRequests", req.MaxKeepAliveRequests)
|
||||
config = confval.Directive.Set(config, "KeepAliveTimeout", req.KeepAliveTimeout)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("apache"); err != nil {
|
||||
out, _ := shell.Execf("%s/server/apache/bin/apachectl configtest", app.Root)
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload apache: %v %s", err, out))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -3,3 +3,19 @@ package apache
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Apache 配置调整
|
||||
type ConfigTune struct {
|
||||
// MPM 事件模型
|
||||
StartServers string `form:"start_servers" json:"start_servers"`
|
||||
MinSpareThreads string `form:"min_spare_threads" json:"min_spare_threads"`
|
||||
MaxSpareThreads string `form:"max_spare_threads" json:"max_spare_threads"`
|
||||
ThreadsPerChild string `form:"threads_per_child" json:"threads_per_child"`
|
||||
MaxRequestWorkers string `form:"max_request_workers" json:"max_request_workers"`
|
||||
MaxConnectionsPerChild string `form:"max_connections_per_child" json:"max_connections_per_child"`
|
||||
// 连接设置
|
||||
Timeout string `form:"timeout" json:"timeout"`
|
||||
KeepAlive string `form:"keep_alive" json:"keep_alive"`
|
||||
MaxKeepAliveRequests string `form:"max_keep_alive_requests" json:"max_keep_alive_requests"`
|
||||
KeepAliveTimeout string `form:"keep_alive_timeout" json:"keep_alive_timeout"`
|
||||
}
|
||||
|
||||
+11
-30
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/v3/internal/apps/opensearch"
|
||||
"github.com/acepanel/panel/v3/internal/apps/percona"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pgadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
@@ -36,34 +37,14 @@ import (
|
||||
)
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
apache.NewApp,
|
||||
clickhouse.NewApp,
|
||||
codeserver.NewApp,
|
||||
docker.NewApp,
|
||||
elasticsearch.NewApp,
|
||||
fail2ban.NewApp,
|
||||
frp.NewApp,
|
||||
gitea.NewApp,
|
||||
grafana.NewApp,
|
||||
kafka.NewApp,
|
||||
mariadb.NewApp,
|
||||
memcached.NewApp,
|
||||
minio.NewApp,
|
||||
mongodb.NewApp,
|
||||
mysql.NewApp,
|
||||
nginx.NewApp,
|
||||
openresty.NewApp,
|
||||
opensearch.NewApp,
|
||||
percona.NewApp,
|
||||
phpmyadmin.NewApp,
|
||||
podman.NewApp,
|
||||
postgresql.NewApp,
|
||||
prometheus.NewApp,
|
||||
pureftpd.NewApp,
|
||||
redis.NewApp,
|
||||
rocketmq.NewApp,
|
||||
rsync.NewApp,
|
||||
s3fs.NewApp,
|
||||
supervisor.NewApp,
|
||||
valkey.NewApp,
|
||||
apache.NewApp, clickhouse.NewApp, codeserver.NewApp,
|
||||
docker.NewApp, elasticsearch.NewApp, fail2ban.NewApp,
|
||||
frp.NewApp, gitea.NewApp, grafana.NewApp,
|
||||
kafka.NewApp, mariadb.NewApp, memcached.NewApp,
|
||||
minio.NewApp, mongodb.NewApp, mysql.NewApp,
|
||||
nginx.NewApp, openresty.NewApp, opensearch.NewApp,
|
||||
percona.NewApp, pgadmin.NewApp, phpmyadmin.NewApp, podman.NewApp,
|
||||
postgresql.NewApp, prometheus.NewApp, pureftpd.NewApp,
|
||||
redis.NewApp, rocketmq.NewApp, rsync.NewApp,
|
||||
s3fs.NewApp, supervisor.NewApp, valkey.NewApp,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package clickhouse
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -9,11 +10,12 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
@@ -27,11 +29,11 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +47,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/default_password", s.SetDefaultPassword)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("clickhouse-server")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// Load 获取 ClickHouse 运行状态
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, _ := systemctl.Status("clickhouse-server")
|
||||
@@ -61,7 +68,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 获取版本
|
||||
versionResp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+version()&user=default&password=%s", port, password))
|
||||
if err != nil || !versionResp.IsSuccess() {
|
||||
if err != nil || !versionResp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
@@ -101,29 +108,12 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("clickhouse-server"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "clickhouse-server")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取配置调整参数
|
||||
@@ -136,14 +126,14 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
ListenHost: s.getYAMLValue(cfg, "listen_host"),
|
||||
HTTPPort: s.getYAMLValue(cfg, "http_port"),
|
||||
TCPPort: s.getYAMLValue(cfg, "tcp_port"),
|
||||
MaxMemoryUsage: s.getYAMLValue(cfg, "max_memory_usage"),
|
||||
MaxThreads: s.getYAMLValue(cfg, "max_threads"),
|
||||
Path: s.getYAMLValue(cfg, "path"),
|
||||
TmpPath: s.getYAMLValue(cfg, "tmp_path"),
|
||||
LogLevel: s.getYAMLValue(cfg, "logger.level"),
|
||||
ListenHost: confval.GetYAML(cfg, "listen_host"),
|
||||
HTTPPort: confval.GetYAML(cfg, "http_port"),
|
||||
TCPPort: confval.GetYAML(cfg, "tcp_port"),
|
||||
MaxMemoryUsage: confval.GetYAML(cfg, "max_memory_usage"),
|
||||
MaxThreads: confval.GetYAML(cfg, "max_threads"),
|
||||
Path: confval.GetYAML(cfg, "path"),
|
||||
TmpPath: confval.GetYAML(cfg, "tmp_path"),
|
||||
LogLevel: confval.GetYAML(cfg, "logger.level"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
@@ -213,7 +203,7 @@ func (s *App) SetDefaultPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 计算 SHA256 哈希
|
||||
hash := sha256.Sum256([]byte(req.Password))
|
||||
hexHash := fmt.Sprintf("%x", hash)
|
||||
hexHash := hex.EncodeToString(hash[:])
|
||||
|
||||
// 读取 users.d/default.yaml 并更新密码
|
||||
raw, _ := io.Read(s.usersConfigPath())
|
||||
@@ -257,12 +247,12 @@ func (s *App) SetDefaultPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// configPath 返回主配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/clickhouse/config/config.yaml", app.Root)
|
||||
return app.Root + "/server/clickhouse/config/config.yaml"
|
||||
}
|
||||
|
||||
// usersConfigPath 返回用户密码配置文件路径(users.d/ 由 ConfigProcessor 自动合并到 users.yaml)
|
||||
func (s *App) usersConfigPath() string {
|
||||
return fmt.Sprintf("%s/server/clickhouse/config/users.d/default.yaml", app.Root)
|
||||
return app.Root + "/server/clickhouse/config/users.d/default.yaml"
|
||||
}
|
||||
|
||||
// getPort 从配置中获取 HTTP 端口
|
||||
@@ -271,35 +261,13 @@ func (s *App) getPort() string {
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := s.getYAMLValue(cfg, "http_port"); v != "" {
|
||||
if v := confval.GetYAML(cfg, "http_port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "8123"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取 YAML 值,支持嵌套键
|
||||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||||
// 先尝试平铺键
|
||||
if val, ok := cfg[key]; ok {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
// 回退到嵌套键
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
val, ok := cfg[parts[0]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
nested, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s.getYAMLValue(nested, parts[1])
|
||||
}
|
||||
|
||||
// setYAMLValue 设置平铺 YAML 值
|
||||
func (s *App) setYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
package clickhouse
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// SetDefaultPassword 设置 default 用户密码
|
||||
type SetDefaultPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
// ConfigTune ClickHouse 配置调整
|
||||
type ConfigTune struct {
|
||||
// 网络
|
||||
ListenHost string `form:"listen_host" json:"listen_host"`
|
||||
HTTPPort string `form:"http_port" json:"http_port"`
|
||||
TCPPort string `form:"tcp_port" json:"tcp_port"`
|
||||
HTTPPort string `form:"http_port" json:"http_port" validate:"number && min:1 && max:65535"`
|
||||
TCPPort string `form:"tcp_port" json:"tcp_port" validate:"number && min:1 && max:65535"`
|
||||
// 性能
|
||||
MaxMemoryUsage string `form:"max_memory_usage" json:"max_memory_usage"`
|
||||
MaxThreads string `form:"max_threads" json:"max_threads"`
|
||||
// 路径
|
||||
Path string `form:"path" json:"path"`
|
||||
TmpPath string `form:"tmp_path" json:"tmp_path"`
|
||||
Path string `form:"path" json:"path" validate:"unix_path"`
|
||||
TmpPath string `form:"tmp_path" json:"tmp_path" validate:"unix_path"`
|
||||
// 日志
|
||||
LogLevel string `form:"log_level" json:"log_level"`
|
||||
LogLevel string `form:"log_level" json:"log_level" validate:"in:trace,debug,information,warning,error"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -21,6 +22,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("code-server")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read("/root/.config/code-server/config.yaml")
|
||||
service.Success(w, config)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Package common 承载各应用共用的 HTTP 处理逻辑
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
)
|
||||
|
||||
// ServeConfig 返回配置文件原始内容
|
||||
func ServeConfig(w http.ResponseWriter, path string) {
|
||||
config, _ := io.Read(path)
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// SaveConfig 写入配置文件并重启对应服务
|
||||
func SaveConfig(w http.ResponseWriter, r *http.Request, path, unit string) {
|
||||
req, err := service.Bind[request.AppUpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(path, req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart(unit); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
// Package confval 读写行式配置文件中的键值项
|
||||
package confval
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Codec 描述一种行式配置文件的书写约定
|
||||
type Codec struct {
|
||||
// Delim 读取时分隔键与值的字符,空表示按空白切分
|
||||
Delim string
|
||||
// Assign 写入时的键值分隔符,如 " "、"="、" = "
|
||||
Assign string
|
||||
// Terminator 行尾终止符,如 nginx 的 ";"
|
||||
Terminator string
|
||||
// Quote 写入时给值添加的引号,读取时一并去除
|
||||
Quote string
|
||||
// Comments 识别为注释的前缀,第一个用于注释掉配置项
|
||||
Comments []string
|
||||
// Sections 是否按 [section] 分组匹配
|
||||
Sections bool
|
||||
// InlineComment 该格式支持行尾注释
|
||||
// 仅对确实支持的格式开启:redis.conf 与 java properties 不支持,
|
||||
// 值里的 "#" 是数据(如密码),误判会损坏配置
|
||||
InlineComment bool
|
||||
// RequireValue 匹配时要求该行已带值,用于区分「开关」与「键 值」
|
||||
RequireValue bool
|
||||
// AppendIndent 追加新配置项时使用的缩进
|
||||
AppendIndent string
|
||||
}
|
||||
|
||||
// line 为一行配置解析后的各组成部分
|
||||
type line struct {
|
||||
indent string // 行首缩进
|
||||
commented string // 命中的注释前缀,非注释行为空
|
||||
key string
|
||||
value string // 已去引号
|
||||
comment string // 行尾注释(含前缀),无则为空
|
||||
ok bool // 是否为可识别的键值行
|
||||
}
|
||||
|
||||
// 各应用配置文件的书写约定
|
||||
var (
|
||||
// Directive 「键 值」,用于 redis、valkey
|
||||
Directive = Codec{Assign: " ", Comments: []string{"#"}}
|
||||
// FTP 「键 值」,pure-ftpd 的配置项必须带值
|
||||
FTP = Codec{Assign: " ", Comments: []string{"#"}, RequireValue: true}
|
||||
// Properties 「键=值」,用于 kafka、rocketmq
|
||||
Properties = Codec{Delim: "=", Assign: "=", Comments: []string{"#"}}
|
||||
// Nginx 「键 值;」,支持行尾注释
|
||||
Nginx = Codec{Assign: " ", Terminator: ";", Comments: []string{"#"}, InlineComment: true, RequireValue: true, AppendIndent: " "}
|
||||
// Postgres 「键 = '值'」,支持行尾注释
|
||||
Postgres = Codec{Delim: "=", Assign: " = ", Quote: "'", Comments: []string{"#"}, InlineComment: true}
|
||||
// INI 「键 = 值」,忽略分组,用于 mysql
|
||||
INI = Codec{Delim: "=", Assign: " = ", Comments: []string{"#", ";"}}
|
||||
// SectionINI 分组感知的 INI,用于 grafana
|
||||
SectionINI = Codec{Delim: "=", Assign: " = ", Comments: []string{"#", ";"}, Sections: true}
|
||||
// PHPINI PHP 的 ini,注释优先使用 ";"
|
||||
PHPINI = Codec{Delim: "=", Assign: " = ", Comments: []string{";", "#"}}
|
||||
// Supervisor 「键=值」,注释同时支持 ";" 与 "#"
|
||||
Supervisor = Codec{Delim: "=", Assign: "=", Comments: []string{";", "#"}}
|
||||
)
|
||||
|
||||
// Get 读取配置项,未命中返回空串
|
||||
func (c Codec) Get(content, key string) string {
|
||||
return c.GetIn(content, "", key)
|
||||
}
|
||||
|
||||
// GetIn 在指定分组内读取配置项,section 为空表示不限分组
|
||||
func (c Codec) GetIn(content, section, key string) string {
|
||||
current := ""
|
||||
for raw := range strings.SplitSeq(content, "\n") {
|
||||
if name, ok := sectionName(raw); ok {
|
||||
current = name
|
||||
continue
|
||||
}
|
||||
if c.Sections && current != section {
|
||||
continue
|
||||
}
|
||||
|
||||
// 被注释掉的配置项视为未设置
|
||||
parsed := c.parse(raw)
|
||||
if parsed.ok && parsed.commented == "" && parsed.key == key {
|
||||
return parsed.value
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Set 写入配置项,值为空时注释掉该项
|
||||
func (c Codec) Set(content, key, value string) string {
|
||||
return c.SetIn(content, "", key, value)
|
||||
}
|
||||
|
||||
// SetIn 在指定分组内写入配置项,分组不存在时在末尾补建
|
||||
func (c Codec) SetIn(content, section, key, value string) string {
|
||||
value = strings.NewReplacer("\n", "", "\r", "").Replace(value)
|
||||
|
||||
raws := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(raws)+3)
|
||||
current := ""
|
||||
found := false
|
||||
// 目标分组最后一行在 result 中的位置,用于在组内补插新项
|
||||
sectionEnd := -1
|
||||
|
||||
for _, raw := range raws {
|
||||
if name, ok := sectionName(raw); ok {
|
||||
current = name
|
||||
result = append(result, raw)
|
||||
continue
|
||||
}
|
||||
if c.Sections && current == section && strings.TrimSpace(raw) != "" {
|
||||
sectionEnd = len(result)
|
||||
}
|
||||
if c.Sections && current != section {
|
||||
result = append(result, raw)
|
||||
continue
|
||||
}
|
||||
|
||||
// 注释掉的配置项同样参与匹配,便于重新启用
|
||||
parsed := c.parse(raw)
|
||||
if !parsed.ok || parsed.key != key {
|
||||
result = append(result, raw)
|
||||
continue
|
||||
}
|
||||
|
||||
// 同名项只保留第一处,后续重复行丢弃
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
|
||||
if value == "" {
|
||||
// 值为空时注释掉该配置项,已注释的原样保留
|
||||
if parsed.commented == "" {
|
||||
result = append(result, parsed.indent+c.comment()+" "+strings.TrimSpace(raw))
|
||||
} else {
|
||||
result = append(result, raw)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 保留原行的缩进与行尾注释,只替换值
|
||||
result = append(result, parsed.indent+c.format(key, value)+trailing(parsed.comment))
|
||||
if c.Sections {
|
||||
sectionEnd = len(result) - 1
|
||||
}
|
||||
}
|
||||
|
||||
if found || value == "" {
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
newLine := c.AppendIndent + c.format(key, value)
|
||||
switch {
|
||||
case !c.Sections:
|
||||
result = append(result, newLine)
|
||||
case sectionEnd >= 0:
|
||||
result = slices.Insert(result, sectionEnd+1, newLine)
|
||||
default:
|
||||
// 分组不存在,在文件末尾补建
|
||||
result = append(result, "", "["+section+"]", newLine)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// parse 将一行拆解为缩进、注释状态、键、值与行尾注释
|
||||
func (c Codec) parse(raw string) line {
|
||||
parsed := line{indent: indentOf(raw)}
|
||||
|
||||
body := strings.TrimSpace(raw)
|
||||
if body == "" {
|
||||
return parsed
|
||||
}
|
||||
if parsed.commented = c.commentOf(body); parsed.commented != "" {
|
||||
body = strings.TrimSpace(strings.TrimPrefix(body, parsed.commented))
|
||||
}
|
||||
|
||||
// 行尾注释在终止符之后,需先摘除
|
||||
if c.InlineComment {
|
||||
body, parsed.comment = c.cutComment(body)
|
||||
}
|
||||
if c.Terminator != "" {
|
||||
if !strings.HasSuffix(body, c.Terminator) {
|
||||
return parsed
|
||||
}
|
||||
body = strings.TrimSpace(strings.TrimSuffix(body, c.Terminator))
|
||||
}
|
||||
|
||||
key, value, ok := c.split(body)
|
||||
if !ok {
|
||||
return parsed
|
||||
}
|
||||
parsed.key, parsed.value, parsed.ok = key, c.unquote(value), true
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
// split 从已剥离注释与终止符的正文中取出键与值
|
||||
func (c Codec) split(body string) (key, value string, ok bool) {
|
||||
if c.Delim != "" {
|
||||
k, v, found := strings.Cut(body, c.Delim)
|
||||
if !found {
|
||||
return "", "", false
|
||||
}
|
||||
return strings.TrimSpace(k), strings.TrimSpace(v), true
|
||||
}
|
||||
|
||||
parts := strings.Fields(body)
|
||||
if len(parts) == 0 || (c.RequireValue && len(parts) < 2) {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
return parts[0], strings.Join(parts[1:], " "), true
|
||||
}
|
||||
|
||||
// cutComment 切出行尾注释,引号内的注释符是数据而非注释
|
||||
func (c Codec) cutComment(body string) (string, string) {
|
||||
inSingle, inDouble := false, false
|
||||
for i := range len(body) {
|
||||
switch ch := body[i]; {
|
||||
case ch == '\'' && !inDouble:
|
||||
inSingle = !inSingle
|
||||
case ch == '"' && !inSingle:
|
||||
inDouble = !inDouble
|
||||
case !inSingle && !inDouble:
|
||||
for _, prefix := range c.Comments {
|
||||
if strings.HasPrefix(body[i:], prefix) {
|
||||
return strings.TrimRight(body[:i], " \t"), body[i:]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return body, ""
|
||||
}
|
||||
|
||||
// unquote 去除值两端的引号
|
||||
func (c Codec) unquote(value string) string {
|
||||
if c.Quote == "" {
|
||||
return value
|
||||
}
|
||||
|
||||
return strings.Trim(value, `'"`)
|
||||
}
|
||||
|
||||
// format 拼出一条完整的配置行
|
||||
func (c Codec) format(key, value string) string {
|
||||
return key + c.Assign + c.Quote + value + c.Quote + c.Terminator
|
||||
}
|
||||
|
||||
// comment 注释掉配置项时使用的前缀
|
||||
func (c Codec) comment() string {
|
||||
if len(c.Comments) == 0 {
|
||||
return "#"
|
||||
}
|
||||
|
||||
return c.Comments[0]
|
||||
}
|
||||
|
||||
// commentOf 返回该行命中的注释前缀,非注释行返回空串
|
||||
func (c Codec) commentOf(trimmed string) string {
|
||||
for _, prefix := range c.Comments {
|
||||
if strings.HasPrefix(trimmed, prefix) {
|
||||
return prefix
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// trailing 把行尾注释接回改写后的行
|
||||
func trailing(comment string) string {
|
||||
if comment == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return " " + comment
|
||||
}
|
||||
|
||||
// sectionName 解析 [section] 行
|
||||
func sectionName(raw string) (string, bool) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true
|
||||
}
|
||||
|
||||
// indentOf 取出行首缩进
|
||||
func indentOf(raw string) string {
|
||||
return raw[:len(raw)-len(strings.TrimLeft(raw, " \t"))]
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package confval
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ConfValTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestConfValTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ConfValTestSuite))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveGet() {
|
||||
conf := "# comment\nbind 127.0.0.1 ::1\nport 6379\n\n# maxmemory 100mb\n"
|
||||
|
||||
s.Equal("127.0.0.1 ::1", Directive.Get(conf, "bind"))
|
||||
s.Equal("6379", Directive.Get(conf, "port"))
|
||||
// 注释掉的项不应被读出
|
||||
s.Empty(Directive.Get(conf, "maxmemory"))
|
||||
s.Empty(Directive.Get(conf, "absent"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveSetReplaces() {
|
||||
s.Equal("port 6380", Directive.Set("port 6379", "port", "6380"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveSetAppendsWhenMissing() {
|
||||
s.Equal("port 6379\ntimeout 30", Directive.Set("port 6379", "timeout", "30"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveSetCommentsOutEmptyValue() {
|
||||
s.Equal("# port 6379", Directive.Set("port 6379", "port", ""))
|
||||
// 已注释的保持原样,不重复加注释符
|
||||
s.Equal("# port 6379", Directive.Set("# port 6379", "port", ""))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveSetReenablesCommented() {
|
||||
s.Equal("maxmemory 200mb", Directive.Set("# maxmemory 100mb", "maxmemory", "200mb"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveSetOnlyKeepsFirstMatch() {
|
||||
s.Equal("port 6380", Directive.Set("port 6379\nport 7000", "port", "6380"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSetStripsNewlinesFromValue() {
|
||||
s.Equal("port 63opq79", Directive.Set("port 1", "port", "63\nopq\r79"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestFTPRequiresValueToMatch() {
|
||||
// 裸键行不算配置项,避免把开关误判为键值对
|
||||
s.Empty(FTP.Get("NoAnonymous\n", "NoAnonymous"))
|
||||
s.Equal("yes", FTP.Get("NoAnonymous yes\n", "NoAnonymous"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestPropertiesRoundTrip() {
|
||||
conf := "# broker\nnum.network.threads=3\n"
|
||||
|
||||
s.Equal("3", Properties.Get(conf, "num.network.threads"))
|
||||
s.Equal("# broker\nnum.network.threads=5\n", Properties.Set(conf, "num.network.threads", "5"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestNginxTerminatorAndIndent() {
|
||||
conf := "http {\n keepalive_timeout 60;\n}"
|
||||
|
||||
s.Equal("60", Nginx.Get(conf, "keepalive_timeout"))
|
||||
// 缩进保留,行尾分号补回
|
||||
s.Equal("http {\n keepalive_timeout 75;\n}", Nginx.Set(conf, "keepalive_timeout", "75"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestNginxIgnoresLinesWithoutTerminator() {
|
||||
s.Empty(Nginx.Get("http {\n keepalive_timeout 60\n}", "keepalive_timeout"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestNginxKeepsTrailingComment() {
|
||||
conf := "http {\n keepalive_timeout 60; # 保持连接\n}"
|
||||
|
||||
// 注释在分号之后,读取时不能混进值里
|
||||
s.Equal("60", Nginx.Get(conf, "keepalive_timeout"))
|
||||
s.Equal("http {\n keepalive_timeout 75; # 保持连接\n}", Nginx.Set(conf, "keepalive_timeout", "75"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestCommentOutKeepsWholeLine() {
|
||||
// 注释掉时整行保留,行尾注释不丢
|
||||
s.Equal("# max_connections = 100 # note", Postgres.Set("max_connections = 100 # note", "max_connections", ""))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestNginxAppendsWithIndent() {
|
||||
s.Equal("a b;\n c d;", Nginx.Set("a b;", "c", "d"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestPostgresQuotesAndInlineComment() {
|
||||
conf := "max_connections = 100 # note\nlisten_addresses = '*'\n"
|
||||
|
||||
s.Equal("100", Postgres.Get(conf, "max_connections"))
|
||||
s.Equal("*", Postgres.Get(conf, "listen_addresses"))
|
||||
// 只替换值,行尾注释保留
|
||||
s.Equal("max_connections = '200' # note\nlisten_addresses = '*'\n", Postgres.Set(conf, "max_connections", "200"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestInlineCommentNotAppliedWhereUnsupported() {
|
||||
// redis.conf 不支持行尾注释,密码里的 # 是数据不能当注释切掉
|
||||
s.Equal("p@ss#word", Directive.Get("requirepass p@ss#word\n", "requirepass"))
|
||||
s.Equal("requirepass a#b", Directive.Set("requirepass p@ss#word", "requirepass", "a#b"))
|
||||
// java properties 同理
|
||||
s.Equal("a#b", Properties.Get("pass=a#b\n", "pass"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestQuotedCommentCharIsData() {
|
||||
s.Equal("a#b", Postgres.Get("password = 'a#b' # real note\n", "password"))
|
||||
s.Equal("password = 'x#y' # real note", Postgres.Set("password = 'a#b' # real note", "password", "x#y"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestINIIgnoresSectionHeaders() {
|
||||
conf := "[mysqld]\nport = 3306\n; skip-name-resolve = 1\n"
|
||||
|
||||
s.Equal("3306", INI.Get(conf, "port"))
|
||||
s.Empty(INI.Get(conf, "mysqld"))
|
||||
// 分号注释同样能被重新启用
|
||||
s.Equal("[mysqld]\nport = 3306\nskip-name-resolve = 2\n", INI.Set(conf, "skip-name-resolve", "2"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSectionINIMatchesOnlyTargetSection() {
|
||||
conf := "[server]\nhttp_port = 3000\n\n[database]\nhttp_port = 9999\n"
|
||||
|
||||
s.Equal("3000", SectionINI.GetIn(conf, "server", "http_port"))
|
||||
s.Equal("9999", SectionINI.GetIn(conf, "database", "http_port"))
|
||||
s.Empty(SectionINI.GetIn(conf, "absent", "http_port"))
|
||||
|
||||
got := SectionINI.SetIn(conf, "database", "http_port", "8888")
|
||||
s.Equal("3000", SectionINI.GetIn(got, "server", "http_port"))
|
||||
s.Equal("8888", SectionINI.GetIn(got, "database", "http_port"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSectionINIInsertsInsideSection() {
|
||||
conf := "[server]\nhttp_port = 3000\n\n[database]\ntype = sqlite3\n"
|
||||
|
||||
got := SectionINI.SetIn(conf, "server", "domain", "example.com")
|
||||
s.Equal("example.com", SectionINI.GetIn(got, "server", "domain"))
|
||||
// 新项必须落在 server 段内,不能漏进 database 段
|
||||
s.Empty(SectionINI.GetIn(got, "database", "domain"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSectionINICreatesMissingSection() {
|
||||
got := SectionINI.SetIn("[server]\nhttp_port = 3000\n", "smtp", "host", "localhost:25")
|
||||
|
||||
s.Equal("localhost:25", SectionINI.GetIn(got, "smtp", "host"))
|
||||
s.Equal("3000", SectionINI.GetIn(got, "server", "http_port"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestPHPINICommentsWithSemicolon() {
|
||||
s.Equal("; memory_limit = 128M", PHPINI.Set("memory_limit = 128M", "memory_limit", ""))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestGetYAMLFlatAndNested() {
|
||||
s.Equal("single-node", GetYAML(map[string]any{"discovery.type": "single-node"}, "discovery.type"))
|
||||
s.Equal("single-node", GetYAML(map[string]any{"discovery": map[string]any{"type": "single-node"}}, "discovery.type"))
|
||||
s.Empty(GetYAML(map[string]any{}, "discovery.type"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSetYAMLFlattensAndClearsNested() {
|
||||
cfg := map[string]any{"discovery": map[string]any{"type": "old"}}
|
||||
SetYAML(cfg, "discovery.type", "single-node")
|
||||
|
||||
s.Equal("single-node", cfg["discovery.type"])
|
||||
// 嵌套键清空后整个父键一并移除
|
||||
_, ok := cfg["discovery"]
|
||||
s.False(ok)
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSetYAMLIgnoresEmptyValue() {
|
||||
cfg := map[string]any{}
|
||||
SetYAML(cfg, "discovery.type", "")
|
||||
|
||||
s.Empty(cfg)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package confval
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// tomlCodec TOML 根表的书写约定,值的引号由 formatTOML 按类型决定
|
||||
var tomlCodec = Codec{Delim: "=", Assign: " = ", Comments: []string{"#"}, InlineComment: true}
|
||||
|
||||
// GetTOML 读取根表配置项,兼容点号键(auth.token = "x")与分组写法([auth] 下的 token = "x")
|
||||
func GetTOML(content, key string) string {
|
||||
section, leaf, nested := cutLast(key)
|
||||
|
||||
current := ""
|
||||
for raw := range strings.SplitSeq(content, "\n") {
|
||||
if name, ok := tableName(raw); ok {
|
||||
current = name
|
||||
continue
|
||||
}
|
||||
|
||||
parsed := tomlCodec.parse(raw)
|
||||
if !parsed.ok || parsed.commented != "" {
|
||||
continue
|
||||
}
|
||||
// 根表下写点号全键,分组内写末段键,两种写法都算命中
|
||||
matched := (current == "" && parsed.key == key) || (nested && current == section && parsed.key == leaf)
|
||||
if matched {
|
||||
return unquoteTOML(parsed.value)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetTOML 写入根表配置项,值为空时注释掉该项;新键以点号形式插入到首个表头之前
|
||||
func SetTOML(content, key string, value any) string {
|
||||
literal := formatTOML(value)
|
||||
section, leaf, nested := cutLast(key)
|
||||
|
||||
raws := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(raws)+1)
|
||||
current := ""
|
||||
found := false
|
||||
// 首个表头在 result 中的位置,新键只能插在它之前才仍属于根表
|
||||
firstTable := -1
|
||||
|
||||
for _, raw := range raws {
|
||||
if name, ok := tableName(raw); ok {
|
||||
current = name
|
||||
if firstTable < 0 {
|
||||
firstTable = len(result)
|
||||
}
|
||||
result = append(result, raw)
|
||||
continue
|
||||
}
|
||||
|
||||
parsed := tomlCodec.parse(raw)
|
||||
matched := parsed.ok && ((current == "" && parsed.key == key) || (nested && current == section && parsed.key == leaf))
|
||||
if !matched || found {
|
||||
result = append(result, raw)
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
|
||||
if literal == "" {
|
||||
// 值为空时注释掉该配置项,已注释的原样保留
|
||||
if parsed.commented == "" {
|
||||
result = append(result, parsed.indent+"# "+strings.TrimSpace(raw))
|
||||
} else {
|
||||
result = append(result, raw)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 保留原行的写法、缩进与行尾注释,只替换值
|
||||
result = append(result, parsed.indent+parsed.key+tomlCodec.Assign+literal+trailing(parsed.comment))
|
||||
}
|
||||
|
||||
if found || literal == "" {
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
if firstTable < 0 {
|
||||
firstTable = len(result)
|
||||
}
|
||||
|
||||
return strings.Join(slices.Insert(result, firstTable, key+tomlCodec.Assign+literal), "\n")
|
||||
}
|
||||
|
||||
// formatTOML 零值返回空串,表示删除该项
|
||||
func formatTOML(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
return ""
|
||||
}
|
||||
return strconv.Quote(v)
|
||||
case bool:
|
||||
return strconv.FormatBool(v)
|
||||
default:
|
||||
return cast.ToString(v)
|
||||
}
|
||||
}
|
||||
|
||||
// unquoteTOML 去除字符串值的引号,非字符串值原样返回
|
||||
func unquoteTOML(value string) string {
|
||||
if unquoted, err := strconv.Unquote(value); err == nil {
|
||||
return unquoted
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// tableName 解析 [table] 与 [[table]] 行,后者比 sectionName 多剥一层括号
|
||||
func tableName(raw string) (string, bool) {
|
||||
name, ok := sectionName(raw)
|
||||
return strings.Trim(name, "[]"), ok
|
||||
}
|
||||
|
||||
// cutLast 切出点号键的分组前缀与末段,如 auth.token -> auth, token
|
||||
func cutLast(key string) (section, leaf string, nested bool) {
|
||||
idx := strings.LastIndex(key, ".")
|
||||
if idx < 0 {
|
||||
return "", key, false
|
||||
}
|
||||
|
||||
return key[:idx], key[idx+1:], true
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package confval
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type TOMLTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestTOMLTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(TOMLTestSuite))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestGetDottedKey() {
|
||||
conf := "bindPort = 7000\nauth.token = \"12345678\"\n# maxPortsPerClient = 0\n"
|
||||
|
||||
s.Equal("7000", GetTOML(conf, "bindPort"))
|
||||
s.Equal("12345678", GetTOML(conf, "auth.token"))
|
||||
// 注释掉的项视为未设置
|
||||
s.Empty(GetTOML(conf, "maxPortsPerClient"))
|
||||
s.Empty(GetTOML(conf, "absent"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestGetSectionKey() {
|
||||
conf := "bindPort = 7000\n\n[auth]\nmethod = \"token\"\ntoken = \"abc\"\n"
|
||||
|
||||
s.Equal("token", GetTOML(conf, "auth.method"))
|
||||
s.Equal("abc", GetTOML(conf, "auth.token"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestGetIgnoresProxyBlock() {
|
||||
conf := "serverAddr = \"1.1.1.1\"\n\n[[proxies]]\nname = \"ssh\"\nserverAddr = \"2.2.2.2\"\n"
|
||||
|
||||
s.Equal("1.1.1.1", GetTOML(conf, "serverAddr"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetReplacesInPlace() {
|
||||
conf := "# 监听端口\nbindPort = 7000 # 默认\nauth.token = \"old\"\n"
|
||||
|
||||
s.Equal("# 监听端口\nbindPort = 7001 # 默认\nauth.token = \"old\"\n", SetTOML(conf, "bindPort", 7001))
|
||||
s.Equal("# 监听端口\nbindPort = 7000 # 默认\nauth.token = \"new\"\n", SetTOML(conf, "auth.token", "new"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetReplacesInSection() {
|
||||
conf := "bindPort = 7000\n\n[auth]\ntoken = \"old\"\n"
|
||||
|
||||
s.Equal("bindPort = 7000\n\n[auth]\ntoken = \"new\"\n", SetTOML(conf, "auth.token", "new"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetInsertsBeforeFirstTable() {
|
||||
conf := "bindPort = 7000\n\n[[proxies]]\nname = \"ssh\"\n"
|
||||
|
||||
s.Equal("bindPort = 7000\n\nauth.token = \"abc\"\n[[proxies]]\nname = \"ssh\"\n", SetTOML(conf, "auth.token", "abc"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetAppendsWhenNoTable() {
|
||||
s.Equal("bindPort = 7000\nauth.token = \"abc\"", SetTOML("bindPort = 7000", "auth.token", "abc"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetCommentsOutEmptyValue() {
|
||||
s.Equal("# bindPort = 7000", SetTOML("bindPort = 7000", "bindPort", ""))
|
||||
s.Equal("# bindPort = 7000", SetTOML("# bindPort = 7000", "bindPort", ""))
|
||||
// 键不存在时不应插入空值
|
||||
s.Equal("bindPort = 7000", SetTOML("bindPort = 7000", "auth.token", ""))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetReenablesCommented() {
|
||||
s.Equal("maxPortsPerClient = 5", SetTOML("# maxPortsPerClient = 0", "maxPortsPerClient", 5))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetBool() {
|
||||
s.Equal("bindPort = 7000\ntransport.tls.force = true", SetTOML("bindPort = 7000", "transport.tls.force", true))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetDoesNotTouchProxyBlock() {
|
||||
conf := "serverAddr = \"1.1.1.1\"\n\n[[proxies]]\n# 手写的隧道\nname = \"ssh\"\nserverPort = 22\n"
|
||||
|
||||
s.Equal("serverAddr = \"2.2.2.2\"\n\n[[proxies]]\n# 手写的隧道\nname = \"ssh\"\nserverPort = 22\n", SetTOML(conf, "serverAddr", "2.2.2.2"))
|
||||
// serverPort 只存在于 proxies 表内,不应被改写,而是插到首个表头前
|
||||
s.Equal("serverAddr = \"1.1.1.1\"\n\nserverPort = 7000\n[[proxies]]\n# 手写的隧道\nname = \"ssh\"\nserverPort = 22\n", SetTOML(conf, "serverPort", 7000))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package confval
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// GetYAML 读取 YAML 配置项,优先平铺键(安装脚本用 sed 生成的格式),回退到嵌套键
|
||||
func GetYAML(cfg map[string]any, key string) string {
|
||||
if val, ok := cfg[key]; ok {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
|
||||
prefix, rest, nested := strings.Cut(key, ".")
|
||||
val, ok := cfg[prefix]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if !nested {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
child, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return GetYAML(child, rest)
|
||||
}
|
||||
|
||||
// SetYAML 以平铺键写入 YAML 配置项,同时清理可能残留的同名嵌套键
|
||||
func SetYAML(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
|
||||
cfg[key] = value
|
||||
prefix, rest, nested := strings.Cut(key, ".")
|
||||
if !nested {
|
||||
return
|
||||
}
|
||||
child, ok := cfg[prefix].(map[string]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(child, rest)
|
||||
if len(child) == 0 {
|
||||
delete(cfg, prefix)
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,11 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -26,34 +28,17 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/settings", s.UpdateSettings)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/docker/daemon.json")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("docker")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.ServeConfig(w, "/etc/docker/daemon.json")
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/docker/daemon.json", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("docker"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, "/etc/docker/daemon.json", "docker")
|
||||
}
|
||||
|
||||
// GetSettings 获取 Docker 设置
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package docker
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// LogOpts 日志配置选项
|
||||
type LogOpts struct {
|
||||
MaxSize string `json:"max-size,omitempty"` // 日志文件最大大小,如 "10m"
|
||||
@@ -12,22 +8,22 @@ type LogOpts struct {
|
||||
|
||||
// Settings Docker daemon 设置
|
||||
type Settings struct {
|
||||
RegistryMirrors []string `json:"registry-mirrors,omitempty"` // 注册表镜像
|
||||
InsecureRegistries []string `json:"insecure-registries,omitempty"` // 非安全镜像仓库
|
||||
LiveRestore bool `json:"live-restore,omitempty"` // Live restore
|
||||
LogDriver string `json:"log-driver,omitempty"` // 日志驱动
|
||||
LogOpts LogOpts `json:"log-opts"` // 日志配置选项
|
||||
CgroupDriver string `json:"cgroup-driver,omitempty"` // cgroup 驱动(从 exec-opts 中提取)
|
||||
Hosts []string `json:"hosts,omitempty"` // Socket 路径
|
||||
DataRoot string `json:"data-root,omitempty"` // 数据目录
|
||||
StorageDriver string `json:"storage-driver,omitempty"` // 存储驱动
|
||||
DNS []string `json:"dns,omitempty"` // DNS 配置
|
||||
FirewallBackend string `json:"firewall-backend,omitempty"` // 防火墙后端 (iptables/nftables)
|
||||
Iptables *bool `json:"iptables,omitempty"` // iptables 规则
|
||||
Ip6tables *bool `json:"ip6tables,omitempty"` // ip6tables 规则
|
||||
IpForward *bool `json:"ip-forward,omitempty"` // IP 转发
|
||||
IPv6 *bool `json:"ipv6,omitempty"` // IPv6 支持
|
||||
Bip string `json:"bip,omitempty"` // 默认 bridge 网络 IP 段
|
||||
RegistryMirrors []string `json:"registry-mirrors,omitempty"` // 注册表镜像
|
||||
InsecureRegistries []string `json:"insecure-registries,omitempty"` // 非安全镜像仓库
|
||||
LiveRestore bool `json:"live-restore,omitempty"` // Live restore
|
||||
LogDriver string `json:"log-driver,omitempty"` // 日志驱动
|
||||
LogOpts LogOpts `json:"log-opts"` // 日志配置选项
|
||||
CgroupDriver string `json:"cgroup-driver,omitempty" validate:"in:systemd,cgroupfs"` // cgroup 驱动(从 exec-opts 中提取)
|
||||
Hosts []string `json:"hosts,omitempty"` // Socket 路径
|
||||
DataRoot string `json:"data-root,omitempty"` // 数据目录
|
||||
StorageDriver string `json:"storage-driver,omitempty"` // 存储驱动
|
||||
DNS []string `json:"dns,omitempty"` // DNS 配置
|
||||
FirewallBackend string `json:"firewall-backend,omitempty" validate:"in:iptables,nftables"` // 防火墙后端 (iptables/nftables)
|
||||
Iptables *bool `json:"iptables,omitempty"` // iptables 规则
|
||||
Ip6tables *bool `json:"ip6tables,omitempty"` // ip6tables 规则
|
||||
IpForward *bool `json:"ip-forward,omitempty"` // IP 转发
|
||||
IPv6 *bool `json:"ipv6,omitempty"` // IPv6 支持
|
||||
Bip string `json:"bip,omitempty"` // 默认 bridge 网络 IP 段
|
||||
}
|
||||
|
||||
// UpdateSettings 更新设置请求
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -15,6 +14,8 @@ import (
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -37,6 +38,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("elasticsearch")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("elasticsearch")
|
||||
if err != nil {
|
||||
@@ -52,7 +58,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/_cluster/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
@@ -87,28 +93,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("elasticsearch"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "elasticsearch")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 ElasticSearch 配置调整参数
|
||||
@@ -130,13 +119,13 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
ClusterName: s.getYAMLValue(cfg, "cluster.name"),
|
||||
NodeName: s.getYAMLValue(cfg, "node.name"),
|
||||
NetworkHost: s.getYAMLValue(cfg, "network.host"),
|
||||
HTTPPort: s.getYAMLValue(cfg, "http.port"),
|
||||
DiscoveryType: s.getYAMLValue(cfg, "discovery.type"),
|
||||
PathData: s.getYAMLValue(cfg, "path.data"),
|
||||
PathLogs: s.getYAMLValue(cfg, "path.logs"),
|
||||
ClusterName: confval.GetYAML(cfg, "cluster.name"),
|
||||
NodeName: confval.GetYAML(cfg, "node.name"),
|
||||
NetworkHost: confval.GetYAML(cfg, "network.host"),
|
||||
HTTPPort: confval.GetYAML(cfg, "http.port"),
|
||||
DiscoveryType: confval.GetYAML(cfg, "discovery.type"),
|
||||
PathData: confval.GetYAML(cfg, "path.data"),
|
||||
PathLogs: confval.GetYAML(cfg, "path.logs"),
|
||||
HeapInitSize: heapInit,
|
||||
HeapMaxSize: heapMax,
|
||||
}
|
||||
@@ -159,13 +148,13 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
s.setYAMLValue(cfg, "cluster.name", req.ClusterName)
|
||||
s.setYAMLValue(cfg, "node.name", req.NodeName)
|
||||
s.setYAMLValue(cfg, "network.host", req.NetworkHost)
|
||||
s.setYAMLValue(cfg, "http.port", req.HTTPPort)
|
||||
s.setYAMLValue(cfg, "discovery.type", req.DiscoveryType)
|
||||
s.setYAMLValue(cfg, "path.data", req.PathData)
|
||||
s.setYAMLValue(cfg, "path.logs", req.PathLogs)
|
||||
confval.SetYAML(cfg, "cluster.name", req.ClusterName)
|
||||
confval.SetYAML(cfg, "node.name", req.NodeName)
|
||||
confval.SetYAML(cfg, "network.host", req.NetworkHost)
|
||||
confval.SetYAML(cfg, "http.port", req.HTTPPort)
|
||||
confval.SetYAML(cfg, "discovery.type", req.DiscoveryType)
|
||||
confval.SetYAML(cfg, "path.data", req.PathData)
|
||||
confval.SetYAML(cfg, "path.logs", req.PathLogs)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
@@ -197,17 +186,17 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/elasticsearch/config/elasticsearch.yml", app.Root)
|
||||
return app.Root + "/server/elasticsearch/config/elasticsearch.yml"
|
||||
}
|
||||
|
||||
// jvmOptionsPath 返回 JVM 选项文件路径
|
||||
func (s *App) jvmOptionsPath() string {
|
||||
return fmt.Sprintf("%s/server/elasticsearch/config/jvm.options", app.Root)
|
||||
return app.Root + "/server/elasticsearch/config/jvm.options"
|
||||
}
|
||||
|
||||
// jvmHeapOptionsPath 返回 JVM 堆内存配置文件路径(ES 9.x 推荐方式)
|
||||
func (s *App) jvmHeapOptionsPath() string {
|
||||
return fmt.Sprintf("%s/server/elasticsearch/config/jvm.options.d/heap.options", app.Root)
|
||||
return app.Root + "/server/elasticsearch/config/jvm.options.d/heap.options"
|
||||
}
|
||||
|
||||
// getPort 从配置中获取 HTTP 端口
|
||||
@@ -216,53 +205,13 @@ func (s *App) getPort() string {
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := s.getYAMLValue(cfg, "http.port"); v != "" {
|
||||
if v := confval.GetYAML(cfg, "http.port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "9200"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取 YAML 值,优先匹配平铺键(如 "path.data"),回退到嵌套键(如 path -> data)
|
||||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||||
// 优先匹配平铺键(安装脚本用 sed 生成的格式)
|
||||
if val, ok := cfg[key]; ok {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
// 回退到嵌套键
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
val, ok := cfg[parts[0]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
nested, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s.getYAMLValue(nested, parts[1])
|
||||
}
|
||||
|
||||
// setYAMLValue 设置 YAML 值
|
||||
func (s *App) setYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
// 使用平铺键,同时清理可能存在的嵌套键
|
||||
cfg[key] = value
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
if len(parts) == 2 {
|
||||
if nested, ok := cfg[parts[0]].(map[string]any); ok {
|
||||
delete(nested, parts[1])
|
||||
if len(nested) == 0 {
|
||||
delete(cfg, parts[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseJVMHeap 从 jvm.options 中提取堆内存配置
|
||||
func (s *App) parseJVMHeap(content string) (initSize, maxSize string) {
|
||||
reInit := regexp.MustCompile(`(?m)^-Xms(\S+)`)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package elasticsearch
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune ElasticSearch 配置调整
|
||||
type ConfigTune struct {
|
||||
// 集群
|
||||
|
||||
+138
-192
@@ -1,22 +1,25 @@
|
||||
package fail2ban
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/utils/str"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
webserver "github.com/acepanel/panel/v3/pkg/webserver/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -24,58 +27,37 @@ type App struct {
|
||||
websiteRepo biz.WebsiteRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, website biz.WebsiteRepo) *App {
|
||||
func NewApp(t *gotext.Locale, websiteRepo biz.WebsiteRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
websiteRepo: website,
|
||||
websiteRepo: websiteRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/jails", s.List)
|
||||
r.Post("/jails", s.Create)
|
||||
r.Delete("/jails", s.Delete)
|
||||
r.Get("/jails/{name}", s.BanList)
|
||||
r.Post("/jails/{name}", s.Update)
|
||||
r.Delete("/jails/{name}", s.Delete)
|
||||
r.Get("/jails/{name}/ban", s.BanList)
|
||||
r.Post("/unban", s.Unban)
|
||||
r.Post("/white_list", s.SetWhiteList)
|
||||
r.Get("/white_list", s.GetWhiteList)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("fail2ban")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// List 所有规则
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
jails, err := listJails()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1)
|
||||
|
||||
jails := make([]Jail, 0)
|
||||
for i, jail := range jailList {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
jailName := jail[1]
|
||||
jailRaw := str.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
|
||||
if len(jailRaw) == 0 {
|
||||
continue
|
||||
}
|
||||
jailEnabled := strings.Contains(jailRaw, "enabled = true")
|
||||
jailMaxRetry := regexp.MustCompile(`maxretry = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailFindTime := regexp.MustCompile(`findtime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailBanTime := regexp.MustCompile(`bantime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
|
||||
jails = append(jails, Jail{
|
||||
Name: jailName,
|
||||
Enabled: jailEnabled,
|
||||
MaxRetry: cast.ToInt(jailMaxRetry[1]),
|
||||
FindTime: cast.ToInt(jailFindTime[1]),
|
||||
BanTime: cast.ToInt(jailBanTime[1]),
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, jails)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
@@ -91,85 +73,39 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
jailName := req.Name
|
||||
jailType := req.Type
|
||||
jailMaxRetry := cast.ToString(req.MaxRetry)
|
||||
jailFindTime := cast.ToString(req.FindTime)
|
||||
jailBanTime := cast.ToString(req.BanTime)
|
||||
jailWebsiteName := req.WebsiteName
|
||||
jailWebsiteMode := req.WebsiteMode
|
||||
jailWebsitePath := req.WebsitePath
|
||||
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if (strings.Contains(raw, "["+jailName+"]") && jailType == "service") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-cc") && jailType == "website" && jailWebsiteMode == "cc") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-path") && jailType == "website" && jailWebsiteMode == "path") {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("rule already exists"))
|
||||
return
|
||||
jail := Jail{
|
||||
Enabled: true,
|
||||
MaxRetry: req.MaxRetry,
|
||||
FindTime: req.FindTime,
|
||||
BanTime: req.BanTime,
|
||||
}
|
||||
|
||||
switch jailType {
|
||||
switch req.Type {
|
||||
case "website":
|
||||
website, err := s.websiteRepo.GetByName(jailWebsiteName)
|
||||
website, err := s.websiteRepo.GetByName(req.WebsiteName)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
var ports string
|
||||
for _, listen := range website.Listens {
|
||||
if port, err := cast.ToIntE(listen.Address); err == nil {
|
||||
ports += fmt.Sprintf("%d", port) + ","
|
||||
}
|
||||
}
|
||||
ports = strings.TrimSuffix(ports, ",")
|
||||
|
||||
rule := `
|
||||
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START
|
||||
[` + jailWebsiteName + `-` + jailWebsiteMode + `]
|
||||
enabled = true
|
||||
filter = haozi-` + jailWebsiteName + `-` + jailWebsiteMode + `
|
||||
port = ` + ports + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
logpath = ` + app.Root + `/sites/` + website.Name + `/log/access.log
|
||||
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-END
|
||||
`
|
||||
raw += rule
|
||||
if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
ports := lo.FilterMap(website.Listens, func(listen webserver.Listen, _ int) (string, bool) {
|
||||
port, err := cast.ToIntE(listen.Address)
|
||||
return strconv.Itoa(port), err == nil
|
||||
})
|
||||
|
||||
var filter string
|
||||
if jailWebsiteMode == "cc" {
|
||||
filter = `
|
||||
[Definition]
|
||||
failregex = ^<HOST>\s-.*HTTP/.*$
|
||||
ignoreregex =
|
||||
`
|
||||
} else {
|
||||
filter = `
|
||||
[Definition]
|
||||
failregex = ^<HOST>\s-.*\s` + jailWebsitePath + `.*HTTP/.*$
|
||||
ignoreregex =
|
||||
`
|
||||
}
|
||||
if err = io.Write("/etc/fail2ban/filter.d/haozi-"+jailWebsiteName+"-"+jailWebsiteMode+".conf", filter, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
jail.Name = req.WebsiteName + "-" + req.WebsiteMode
|
||||
jail.Filter = panelFilterPrefix + jail.Name
|
||||
jail.Port = strings.Join(ports, ",")
|
||||
jail.LogPath = app.Root + "/sites/" + website.Name + "/log/access.log"
|
||||
|
||||
case "service":
|
||||
var filter string
|
||||
var port string
|
||||
var filter, port string
|
||||
var err error
|
||||
switch jailName {
|
||||
switch req.Name {
|
||||
case "ssh":
|
||||
filter = "sshd"
|
||||
port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}' | paste -sd ','")
|
||||
case "mysql":
|
||||
filter = "mysqld-auth"
|
||||
port, err = shell.Execf("cat %s/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'", app.Root)
|
||||
@@ -185,104 +121,132 @@ ignoreregex =
|
||||
return
|
||||
}
|
||||
|
||||
rule := `
|
||||
# ` + jailName + `-START
|
||||
[` + jailName + `]
|
||||
enabled = true
|
||||
filter = ` + filter + `
|
||||
port = ` + port + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
# ` + jailName + `-END
|
||||
`
|
||||
raw += rule
|
||||
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
jail.Name = req.Name
|
||||
jail.Filter = filter
|
||||
jail.Port = port
|
||||
}
|
||||
|
||||
if io.Exists(jailPath(jail.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("rule already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
// 网站规则的过滤器由面板生成,服务规则复用 fail2ban 自带的
|
||||
if req.Type == "website" {
|
||||
failRegex := `^<HOST>\s-.*HTTP/.*$`
|
||||
if req.WebsiteMode == "path" {
|
||||
failRegex = `^<HOST>\s-.*\s` + req.WebsitePath + `.*HTTP/.*$`
|
||||
}
|
||||
filter := "[Definition]\nfailregex = " + failRegex + "\nignoreregex =\n"
|
||||
if err = io.Write(filterPath(jail.Filter), filter, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("fail2ban-client reload"); err != nil {
|
||||
if err = writeJail(jail); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
s.reload(w)
|
||||
}
|
||||
|
||||
// Delete 删除规则
|
||||
func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Delete](r)
|
||||
// Update 修改规则
|
||||
func (s *App) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Update](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
jail, err := readJail(req.Name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(raw, "["+req.Name+"]") {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("rule not found"))
|
||||
return
|
||||
}
|
||||
|
||||
rule := str.Cut(raw, "# "+req.Name+"-START", "# "+req.Name+"-END")
|
||||
raw = strings.ReplaceAll(raw, "\n# "+req.Name+"-START"+rule+"# "+req.Name+"-END", "")
|
||||
raw = strings.TrimSpace(raw)
|
||||
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
jail.Enabled = req.Enabled
|
||||
jail.MaxRetry = req.MaxRetry
|
||||
jail.FindTime = req.FindTime
|
||||
jail.BanTime = req.BanTime
|
||||
|
||||
if err = writeJail(jail); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
s.reload(w)
|
||||
}
|
||||
|
||||
// BanList 获取封禁列表
|
||||
func (s *App) BanList(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[BanList](r)
|
||||
// Delete 删除规则
|
||||
func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[JailName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
currentlyBan, err := shell.Execf(`fail2ban-client status %s | grep "Currently banned" | awk '{print $4}'`, req.Name)
|
||||
jail, err := readJail(req.Name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get current banned list"))
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("rule not found"))
|
||||
return
|
||||
}
|
||||
totalBan, err := shell.Execf(`fail2ban-client status %s | grep "Total banned" | awk '{print $4}'`, req.Name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get total banned list"))
|
||||
return
|
||||
}
|
||||
bannedIp, err := shell.Execf(`fail2ban-client status %s | grep "Banned IP list" | sed 's/.*Banned IP list:[[:space:]]*//'`, req.Name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get banned ip list"))
|
||||
return
|
||||
}
|
||||
bannedIpList := strings.Split(bannedIp, " ")
|
||||
|
||||
var list []map[string]string
|
||||
for _, ip := range bannedIpList {
|
||||
if len(ip) > 0 {
|
||||
list = append(list, map[string]string{
|
||||
"name": req.Name,
|
||||
"ip": ip,
|
||||
})
|
||||
if err = io.Remove(jailPath(jail.Name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
// 面板生成的过滤器随规则一起删除,fail2ban 自带的保留
|
||||
if strings.HasPrefix(jail.Filter, panelFilterPrefix) {
|
||||
if err = io.Remove(filterPath(jail.Filter)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if list == nil {
|
||||
list = []map[string]string{}
|
||||
|
||||
s.reload(w)
|
||||
}
|
||||
|
||||
// BanList 获取封禁列表
|
||||
func (s *App) BanList(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[JailName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := shell.Execf("fail2ban-client status %s", req.Name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get the status of rule %s: %v", req.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
// 输出形如 " |- Currently banned: 1",行首有树状前缀,按首个冒号切分后比对尾部
|
||||
var currentlyBan, totalBan, bannedIP string
|
||||
for line := range strings.SplitSeq(out, "\n") {
|
||||
label, value, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(label, "Currently banned"):
|
||||
currentlyBan = strings.TrimSpace(value)
|
||||
case strings.HasSuffix(label, "Total banned"):
|
||||
totalBan = strings.TrimSpace(value)
|
||||
case strings.HasSuffix(label, "Banned IP list"):
|
||||
bannedIP = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
|
||||
list := lo.Map(strings.Fields(bannedIP), func(ip string, _ int) map[string]string {
|
||||
return map[string]string{
|
||||
"name": req.Name,
|
||||
"ip": ip,
|
||||
}
|
||||
})
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"currently_ban": currentlyBan,
|
||||
"total_ban": totalBan,
|
||||
@@ -314,45 +278,27 @@ func (s *App) SetWhiteList(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
// 正则替换
|
||||
reg := regexp.MustCompile(`ignoreip\s*=\s*.*\n`)
|
||||
if reg.MatchString(raw) {
|
||||
raw = reg.ReplaceAllString(raw, "ignoreip = "+req.IP+"\n")
|
||||
} else {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to parse the ignoreip of fail2ban"))
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
raw, _ := io.Read(jailLocal)
|
||||
if err = io.Write(jailLocal, confval.SectionINI.SetIn(raw, "DEFAULT", "ignoreip", req.IP), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("fail2ban-client reload"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
service.Success(w, nil)
|
||||
s.reload(w)
|
||||
}
|
||||
|
||||
// GetWhiteList 获取白名单
|
||||
func (s *App) GetWhiteList(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
reg := regexp.MustCompile(`ignoreip\s*=\s*(.*)\n`)
|
||||
if reg.MatchString(raw) {
|
||||
ignoreIp := reg.FindStringSubmatch(raw)[1]
|
||||
service.Success(w, ignoreIp)
|
||||
} else {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to parse the ignoreip of fail2ban"))
|
||||
return
|
||||
}
|
||||
raw, _ := io.Read(jailLocal)
|
||||
|
||||
service.Success(w, confval.SectionINI.GetIn(raw, "DEFAULT", "ignoreip"))
|
||||
}
|
||||
|
||||
func (s *App) reload(w http.ResponseWriter) {
|
||||
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package fail2ban
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
)
|
||||
|
||||
const (
|
||||
// jailLocal 只承载 [DEFAULT] 全局项,规则一律放 jail.d
|
||||
jailLocal = "/etc/fail2ban/jail.local"
|
||||
// jailDDir fail2ban 原生的 drop-in 目录,.local 后缀的读取优先级最高
|
||||
jailDDir = "/etc/fail2ban/jail.d"
|
||||
filterDir = "/etc/fail2ban/filter.d"
|
||||
// panelFilterPrefix 面板为网站规则生成的过滤器前缀,用于区分 fail2ban 自带的过滤器
|
||||
panelFilterPrefix = "haozi-"
|
||||
)
|
||||
|
||||
func jailPath(name string) string {
|
||||
return filepath.Join(jailDDir, name+".local")
|
||||
}
|
||||
|
||||
func filterPath(filter string) string {
|
||||
return filepath.Join(filterDir, filter+".conf")
|
||||
}
|
||||
|
||||
func readJail(name string) (Jail, error) {
|
||||
raw, err := io.Read(jailPath(name))
|
||||
if err != nil {
|
||||
return Jail{}, err
|
||||
}
|
||||
|
||||
return Jail{
|
||||
Name: name,
|
||||
Enabled: cast.ToBool(confval.SectionINI.GetIn(raw, name, "enabled")),
|
||||
MaxRetry: cast.ToInt(confval.SectionINI.GetIn(raw, name, "maxretry")),
|
||||
FindTime: cast.ToInt(confval.SectionINI.GetIn(raw, name, "findtime")),
|
||||
BanTime: cast.ToInt(confval.SectionINI.GetIn(raw, name, "bantime")),
|
||||
Filter: confval.SectionINI.GetIn(raw, name, "filter"),
|
||||
Port: confval.SectionINI.GetIn(raw, name, "port"),
|
||||
LogPath: confval.SectionINI.GetIn(raw, name, "logpath"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func listJails() ([]Jail, error) {
|
||||
// Glob 在目录不存在时返回空,无需额外判断
|
||||
paths, err := filepath.Glob(jailPath("*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jails := make([]Jail, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
jail, err := readJail(strings.TrimSuffix(filepath.Base(path), ".local"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jails = append(jails, jail)
|
||||
}
|
||||
|
||||
return jails, nil
|
||||
}
|
||||
|
||||
func writeJail(jail Jail) error {
|
||||
config := fmt.Sprintf(
|
||||
"[%s]\nenabled = %t\nfilter = %s\nport = %s\nmaxretry = %d\nfindtime = %d\nbantime = %d\n",
|
||||
jail.Name, jail.Enabled, jail.Filter, jail.Port, jail.MaxRetry, jail.FindTime, jail.BanTime,
|
||||
)
|
||||
if jail.LogPath != "" {
|
||||
config += "backend = auto\nlogpath = " + jail.LogPath + "\n"
|
||||
}
|
||||
|
||||
return io.Write(jailPath(jail.Name), config, 0644)
|
||||
}
|
||||
@@ -1,27 +1,32 @@
|
||||
package fail2ban
|
||||
|
||||
type Add struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
MaxRetry int `json:"maxretry" validate:"required"`
|
||||
FindTime int `json:"findtime" validate:"required"`
|
||||
BanTime int `json:"bantime" validate:"required"`
|
||||
WebsiteName string `json:"website_name"`
|
||||
WebsiteMode string `json:"website_mode"`
|
||||
Name string `json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
Type string `json:"type" validate:"required && in:service,website"`
|
||||
MaxRetry int `json:"maxretry" validate:"required && min:1"`
|
||||
FindTime int `json:"findtime" validate:"required && min:1"`
|
||||
BanTime int `json:"bantime" validate:"required && min:1"`
|
||||
WebsiteName string `json:"website_name" validate:"required_if:Type,website"`
|
||||
WebsiteMode string `json:"website_mode" validate:"required_if:Type,website && in:cc,path"`
|
||||
WebsitePath string `json:"website_path"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
type JailName struct {
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
}
|
||||
|
||||
type BanList struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
// Update 只允许改这几项,过滤器、端口与日志路径随规则文件原样保留
|
||||
type Update struct {
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
Enabled bool `json:"enabled"`
|
||||
MaxRetry int `json:"max_retry" validate:"required && min:1"`
|
||||
FindTime int `json:"find_time" validate:"required && min:1"`
|
||||
BanTime int `json:"ban_time" validate:"required && min:1"`
|
||||
}
|
||||
|
||||
type Unban struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
IP string `json:"ip" validate:"required"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
IP string `json:"ip" validate:"required && ip"`
|
||||
}
|
||||
|
||||
type SetWhiteList struct {
|
||||
|
||||
@@ -6,4 +6,9 @@ type Jail struct {
|
||||
MaxRetry int `json:"max_retry"`
|
||||
FindTime int `json:"find_time"`
|
||||
BanTime int `json:"ban_time"`
|
||||
|
||||
// 以下几项在新增时由面板推导,之后随规则文件原样保留
|
||||
Filter string `json:"filter"`
|
||||
Port string `json:"port"`
|
||||
LogPath string `json:"log_path"`
|
||||
}
|
||||
|
||||
+311
-14
@@ -5,17 +5,23 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -23,6 +29,26 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/user", s.GetUser)
|
||||
r.Post("/user", s.UpdateUser)
|
||||
r.Get("/server", s.GetServer)
|
||||
r.Post("/server", s.UpdateServer)
|
||||
r.Get("/client", s.GetClient)
|
||||
r.Post("/client", s.UpdateClient)
|
||||
r.Get("/proxies", s.Proxies)
|
||||
r.Post("/proxies", s.CreateProxy)
|
||||
r.Get("/proxies/{name}", s.GetProxy)
|
||||
r.Post("/proxies/{name}", s.UpdateProxy)
|
||||
r.Delete("/proxies/{name}", s.DeleteProxy)
|
||||
r.Get("/visitors", s.Visitors)
|
||||
r.Post("/visitors", s.CreateVisitor)
|
||||
r.Get("/visitors/{name}", s.GetVisitor)
|
||||
r.Post("/visitors/{name}", s.UpdateVisitor)
|
||||
r.Delete("/visitors/{name}", s.DeleteVisitor)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
frps, _ := systemctl.Status("frps")
|
||||
frpc, _ := systemctl.Status("frpc")
|
||||
return types.AggregateAppStatus(frps, frpc)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -32,7 +58,7 @@ func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/frp/%s.toml", app.Root, req.Name))
|
||||
config, err := io.Read(confPath(req.Name))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -48,7 +74,7 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/frp/%s.toml", app.Root, req.Name), req.Config, 0644); err != nil {
|
||||
if err = io.Write(confPath(req.Name), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -107,18 +133,19 @@ func (s *App) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
hasGroup := groupRegex.MatchString(content)
|
||||
|
||||
// 替换或添加 User 和 Group 配置
|
||||
if hasUser && hasGroup {
|
||||
switch {
|
||||
case hasUser && hasGroup:
|
||||
// 两者都存在,分别替换
|
||||
content = userRegex.ReplaceAllString(content, fmt.Sprintf("User=%s", req.User))
|
||||
content = groupRegex.ReplaceAllString(content, fmt.Sprintf("Group=%s", req.Group))
|
||||
} else if hasUser && !hasGroup {
|
||||
content = userRegex.ReplaceAllString(content, "User="+req.User)
|
||||
content = groupRegex.ReplaceAllString(content, "Group="+req.Group)
|
||||
case hasUser:
|
||||
// 只有 User,替换 User 并添加 Group
|
||||
content = userRegex.ReplaceAllString(content, fmt.Sprintf("User=%s\nGroup=%s", req.User, req.Group))
|
||||
} else if !hasUser && hasGroup {
|
||||
case hasGroup:
|
||||
// 只有 Group,添加 User 并替换 Group
|
||||
content = serviceRegex.ReplaceAllString(content, fmt.Sprintf("[Service]\nUser=%s", req.User))
|
||||
content = groupRegex.ReplaceAllString(content, fmt.Sprintf("Group=%s", req.Group))
|
||||
} else {
|
||||
content = serviceRegex.ReplaceAllString(content, "[Service]\nUser="+req.User)
|
||||
content = groupRegex.ReplaceAllString(content, "Group="+req.Group)
|
||||
default:
|
||||
// 两者都不存在,在 [Service] 后添加两者
|
||||
content = serviceRegex.ReplaceAllString(content, fmt.Sprintf("[Service]\nUser=%s\nGroup=%s", req.User, req.Group))
|
||||
}
|
||||
@@ -139,3 +166,273 @@ func (s *App) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetServer 获取 frps 可视化参数
|
||||
func (s *App) GetServer(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(confPath("frps"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := new(ServerTune)
|
||||
readTune(config, serverFields(tune))
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateServer 更新 frps 可视化参数
|
||||
func (s *App) UpdateServer(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ServerTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
path := confPath("frps")
|
||||
config, err := io.Read(path)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(path, writeTune(config, serverFields(req)), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("frps"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetClient 获取 frpc 可视化公共参数
|
||||
func (s *App) GetClient(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(confPath("frpc"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := new(ClientTune)
|
||||
readTune(config, clientFields(tune))
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateClient 更新 frpc 可视化公共参数
|
||||
func (s *App) UpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ClientTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
path := confPath("frpc")
|
||||
config, err := io.Read(path)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(path, writeTune(config, clientFields(req)), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("frpc"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// Proxies 代理列表
|
||||
func (s *App) Proxies(w http.ResponseWriter, r *http.Request) {
|
||||
confD, err := listConfD()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, confD.Proxies)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateProxy 新增代理
|
||||
func (s *App) CreateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Proxy](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if io.Exists(itemPath(proxyPrefix, req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("proxy %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
s.save(w, proxyPrefix, req.Name, ConfD{Proxies: []Proxy{*req}})
|
||||
}
|
||||
|
||||
// GetProxy 获取单个代理
|
||||
func (s *App) GetProxy(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ItemName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confD, err := readConfD(itemPath(proxyPrefix, req.Name))
|
||||
if err != nil || len(confD.Proxies) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("proxy %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, confD.Proxies[0])
|
||||
}
|
||||
|
||||
// UpdateProxy 更新代理
|
||||
func (s *App) UpdateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Proxy](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !io.Exists(itemPath(proxyPrefix, req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("proxy %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
s.save(w, proxyPrefix, req.Name, ConfD{Proxies: []Proxy{*req}})
|
||||
}
|
||||
|
||||
// DeleteProxy 删除代理
|
||||
func (s *App) DeleteProxy(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ItemName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Remove(itemPath(proxyPrefix, req.Name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("frpc"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// Visitors 访问者列表
|
||||
func (s *App) Visitors(w http.ResponseWriter, r *http.Request) {
|
||||
confD, err := listConfD()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, confD.Visitors)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateVisitor 新增访问者
|
||||
func (s *App) CreateVisitor(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Visitor](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if io.Exists(itemPath(visitorPrefix, req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("visitor %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
s.save(w, visitorPrefix, req.Name, ConfD{Visitors: []Visitor{*req}})
|
||||
}
|
||||
|
||||
// GetVisitor 获取单个访问者
|
||||
func (s *App) GetVisitor(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ItemName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confD, err := readConfD(itemPath(visitorPrefix, req.Name))
|
||||
if err != nil || len(confD.Visitors) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("visitor %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, confD.Visitors[0])
|
||||
}
|
||||
|
||||
// UpdateVisitor 更新访问者
|
||||
func (s *App) UpdateVisitor(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Visitor](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !io.Exists(itemPath(visitorPrefix, req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("visitor %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
s.save(w, visitorPrefix, req.Name, ConfD{Visitors: []Visitor{*req}})
|
||||
}
|
||||
|
||||
// DeleteVisitor 删除访问者
|
||||
func (s *App) DeleteVisitor(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ItemName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Remove(itemPath(visitorPrefix, req.Name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("frpc"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) save(w http.ResponseWriter, prefix, name string, confD ConfD) {
|
||||
if err := writeConfD(prefix, name, confD); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Restart("frpc"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package frp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
)
|
||||
|
||||
const (
|
||||
proxyPrefix = "proxy"
|
||||
visitorPrefix = "visitor"
|
||||
)
|
||||
|
||||
func confPath(name string) string {
|
||||
return filepath.Join(app.Root, "server", "frp", name+".toml")
|
||||
}
|
||||
|
||||
func confDDir() string {
|
||||
return filepath.Join(app.Root, "server", "frp", "conf.d")
|
||||
}
|
||||
|
||||
func confDGlob() string {
|
||||
return filepath.Join(confDDir(), "*.toml")
|
||||
}
|
||||
|
||||
func itemPath(prefix, name string) string {
|
||||
return filepath.Join(confDDir(), prefix+"-"+name+".toml")
|
||||
}
|
||||
|
||||
func readConfD(path string) (ConfD, error) {
|
||||
raw, err := io.Read(path)
|
||||
if err != nil {
|
||||
return ConfD{}, err
|
||||
}
|
||||
|
||||
var confD ConfD
|
||||
return confD, toml.Unmarshal([]byte(raw), &confD)
|
||||
}
|
||||
|
||||
func listConfD() (ConfD, error) {
|
||||
var all ConfD
|
||||
|
||||
// Glob 在目录不存在时返回空,无需额外判断
|
||||
paths, err := filepath.Glob(confDGlob())
|
||||
if err != nil {
|
||||
return all, err
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
confD, err := readConfD(path)
|
||||
if err != nil {
|
||||
return all, err
|
||||
}
|
||||
|
||||
all.Proxies = append(all.Proxies, confD.Proxies...)
|
||||
all.Visitors = append(all.Visitors, confD.Visitors...)
|
||||
}
|
||||
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func writeConfD(prefix, name string, confD ConfD) error {
|
||||
if err := os.MkdirAll(confDDir(), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
raw, err := toml.Marshal(confD)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return io.Write(itemPath(prefix, name), string(raw), 0644)
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
package frp
|
||||
|
||||
type Name struct {
|
||||
Name string `form:"name" json:"name" validate:"required|in:frps,frpc"`
|
||||
Name string `form:"name" json:"name" validate:"required && in:frps,frpc"`
|
||||
}
|
||||
|
||||
type UpdateConfig struct {
|
||||
Name string `form:"name" json:"name" validate:"required|in:frps,frpc"`
|
||||
Name string `form:"name" json:"name" validate:"required && in:frps,frpc"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateUser struct {
|
||||
Name string `form:"name" json:"name" validate:"required|in:frps,frpc"`
|
||||
User string `form:"user" json:"user" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Group string `form:"group" json:"group" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Name string `form:"name" json:"name" validate:"required && in:frps,frpc"`
|
||||
User string `form:"user" json:"user" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
|
||||
Group string `form:"group" json:"group" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
|
||||
}
|
||||
|
||||
// ItemName 代理或访问者的名称,来自路径参数
|
||||
type ItemName struct {
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package frp
|
||||
|
||||
import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
)
|
||||
|
||||
type tuneKind uint8
|
||||
|
||||
const (
|
||||
tuneString tuneKind = iota
|
||||
tuneInt
|
||||
tuneBool
|
||||
)
|
||||
|
||||
// tuneField 可视化参数与 TOML 键的对应关系,表单一律用字符串收值,kind 决定写回时的字面量类型
|
||||
type tuneField struct {
|
||||
key string
|
||||
value *string
|
||||
kind tuneKind
|
||||
}
|
||||
|
||||
// literal 空串表示注释掉该项
|
||||
func (f tuneField) literal() any {
|
||||
if *f.value == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch f.kind {
|
||||
case tuneInt:
|
||||
return cast.ToInt(*f.value)
|
||||
case tuneBool:
|
||||
return cast.ToBool(*f.value)
|
||||
default:
|
||||
return *f.value
|
||||
}
|
||||
}
|
||||
|
||||
func readTune(config string, fields []tuneField) {
|
||||
for _, f := range fields {
|
||||
*f.value = confval.GetTOML(config, f.key)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTune(config string, fields []tuneField) string {
|
||||
for _, f := range fields {
|
||||
config = confval.SetTOML(config, f.key, f.literal())
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func serverFields(t *ServerTune) []tuneField {
|
||||
return []tuneField{
|
||||
{"bindAddr", &t.BindAddr, tuneString},
|
||||
{"bindPort", &t.BindPort, tuneInt},
|
||||
{"kcpBindPort", &t.KCPBindPort, tuneInt},
|
||||
{"quicBindPort", &t.QUICBindPort, tuneInt},
|
||||
{"proxyBindAddr", &t.ProxyBindAddr, tuneString},
|
||||
{"subDomainHost", &t.SubDomainHost, tuneString},
|
||||
{"maxPortsPerClient", &t.MaxPortsPerClient, tuneInt},
|
||||
{"udpPacketSize", &t.UDPPacketSize, tuneInt},
|
||||
{"detailedErrorsToClient", &t.DetailedErrorsToClient, tuneBool},
|
||||
|
||||
{"auth.method", &t.AuthMethod, tuneString},
|
||||
{"auth.token", &t.AuthToken, tuneString},
|
||||
|
||||
{"vhostHTTPPort", &t.VhostHTTPPort, tuneInt},
|
||||
{"vhostHTTPSPort", &t.VhostHTTPSPort, tuneInt},
|
||||
{"vhostHTTPTimeout", &t.VhostHTTPTimeout, tuneInt},
|
||||
{"tcpmuxHTTPConnectPort", &t.TCPMuxHTTPConnectPort, tuneInt},
|
||||
{"custom404Page", &t.Custom404Page, tuneString},
|
||||
|
||||
{"webServer.addr", &t.WebServerAddr, tuneString},
|
||||
{"webServer.port", &t.WebServerPort, tuneInt},
|
||||
{"webServer.user", &t.WebServerUser, tuneString},
|
||||
{"webServer.password", &t.WebServerPassword, tuneString},
|
||||
{"webServer.pprofEnable", &t.WebServerPprofEnable, tuneBool},
|
||||
{"enablePrometheus", &t.EnablePrometheus, tuneBool},
|
||||
|
||||
{"transport.maxPoolCount", &t.TransportMaxPoolCount, tuneInt},
|
||||
{"transport.tcpMux", &t.TransportTCPMux, tuneBool},
|
||||
{"transport.tcpMuxKeepaliveInterval", &t.TransportTCPMuxKeepaliveInterval, tuneInt},
|
||||
{"transport.tcpKeepalive", &t.TransportTCPKeepalive, tuneInt},
|
||||
{"transport.heartbeatTimeout", &t.TransportHeartbeatTimeout, tuneInt},
|
||||
{"transport.tls.force", &t.TransportTLSForce, tuneBool},
|
||||
{"transport.tls.certFile", &t.TransportTLSCertFile, tuneString},
|
||||
{"transport.tls.keyFile", &t.TransportTLSKeyFile, tuneString},
|
||||
{"transport.tls.trustedCaFile", &t.TransportTLSTrustedCaFile, tuneString},
|
||||
|
||||
{"log.to", &t.LogTo, tuneString},
|
||||
{"log.level", &t.LogLevel, tuneString},
|
||||
{"log.maxDays", &t.LogMaxDays, tuneInt},
|
||||
{"log.disablePrintColor", &t.LogDisablePrintColor, tuneBool},
|
||||
}
|
||||
}
|
||||
|
||||
func clientFields(t *ClientTune) []tuneField {
|
||||
return []tuneField{
|
||||
{"user", &t.User, tuneString},
|
||||
{"serverAddr", &t.ServerAddr, tuneString},
|
||||
{"serverPort", &t.ServerPort, tuneInt},
|
||||
{"loginFailExit", &t.LoginFailExit, tuneBool},
|
||||
{"natHoleStunServer", &t.NatHoleStunServer, tuneString},
|
||||
{"dnsServer", &t.DNSServer, tuneString},
|
||||
{"udpPacketSize", &t.UDPPacketSize, tuneInt},
|
||||
|
||||
{"auth.method", &t.AuthMethod, tuneString},
|
||||
{"auth.token", &t.AuthToken, tuneString},
|
||||
|
||||
{"transport.protocol", &t.TransportProtocol, tuneString},
|
||||
{"transport.poolCount", &t.TransportPoolCount, tuneInt},
|
||||
{"transport.tcpMux", &t.TransportTCPMux, tuneBool},
|
||||
{"transport.tcpMuxKeepaliveInterval", &t.TransportTCPMuxKeepaliveInterval, tuneInt},
|
||||
{"transport.dialServerTimeout", &t.TransportDialServerTimeout, tuneInt},
|
||||
{"transport.dialServerKeepalive", &t.TransportDialServerKeepalive, tuneInt},
|
||||
{"transport.heartbeatInterval", &t.TransportHeartbeatInterval, tuneInt},
|
||||
{"transport.heartbeatTimeout", &t.TransportHeartbeatTimeout, tuneInt},
|
||||
{"transport.connectServerLocalIP", &t.TransportConnectServerLocalIP, tuneString},
|
||||
{"transport.proxyURL", &t.TransportProxyURL, tuneString},
|
||||
{"transport.tls.enable", &t.TransportTLSEnable, tuneBool},
|
||||
{"transport.tls.certFile", &t.TransportTLSCertFile, tuneString},
|
||||
{"transport.tls.keyFile", &t.TransportTLSKeyFile, tuneString},
|
||||
{"transport.tls.trustedCaFile", &t.TransportTLSTrustedCaFile, tuneString},
|
||||
{"transport.tls.serverName", &t.TransportTLSServerName, tuneString},
|
||||
|
||||
{"webServer.addr", &t.WebServerAddr, tuneString},
|
||||
{"webServer.port", &t.WebServerPort, tuneInt},
|
||||
{"webServer.user", &t.WebServerUser, tuneString},
|
||||
{"webServer.password", &t.WebServerPassword, tuneString},
|
||||
{"webServer.pprofEnable", &t.WebServerPprofEnable, tuneBool},
|
||||
|
||||
{"log.to", &t.LogTo, tuneString},
|
||||
{"log.level", &t.LogLevel, tuneString},
|
||||
{"log.maxDays", &t.LogMaxDays, tuneInt},
|
||||
{"log.disablePrintColor", &t.LogDisablePrintColor, tuneBool},
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,216 @@ type UserInfo struct {
|
||||
User string `json:"user"`
|
||||
Group string `json:"group"`
|
||||
}
|
||||
|
||||
// ConfD conf.d 目录下的配置文件,只承载代理与访问者
|
||||
type ConfD struct {
|
||||
Proxies []Proxy `toml:"proxies,omitempty"`
|
||||
Visitors []Visitor `toml:"visitors,omitempty"`
|
||||
}
|
||||
|
||||
// Proxy 代理配置,字段对齐 frp pkg/config/v1 的 ProxyConfigurer 实现
|
||||
type Proxy struct {
|
||||
Name string `toml:"name" form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
Type string `toml:"type" json:"type" validate:"required && in:tcp,udp,http,https,tcpmux,stcp,sudp,xtcp"`
|
||||
Enabled *bool `toml:"enabled,omitempty" json:"enabled"`
|
||||
|
||||
LocalIP string `toml:"localIP,omitempty" json:"local_ip"`
|
||||
LocalPort int `toml:"localPort,omitempty" json:"local_port"`
|
||||
|
||||
// tcp、udp
|
||||
RemotePort int `toml:"remotePort,omitempty" json:"remote_port"`
|
||||
|
||||
// http、https、tcpmux
|
||||
CustomDomains []string `toml:"customDomains,omitempty" json:"custom_domains"`
|
||||
Subdomain string `toml:"subdomain,omitempty" json:"subdomain"`
|
||||
|
||||
// http、tcpmux
|
||||
HTTPUser string `toml:"httpUser,omitempty" json:"http_user"`
|
||||
HTTPPassword string `toml:"httpPassword,omitempty" json:"http_password"`
|
||||
RouteByHTTPUser string `toml:"routeByHTTPUser,omitempty" json:"route_by_http_user"`
|
||||
|
||||
// http
|
||||
Locations []string `toml:"locations,omitempty" json:"locations"`
|
||||
HostHeaderRewrite string `toml:"hostHeaderRewrite,omitempty" json:"host_header_rewrite"`
|
||||
RequestHeaders *HeaderOperations `toml:"requestHeaders,omitempty" json:"request_headers"`
|
||||
ResponseHeaders *HeaderOperations `toml:"responseHeaders,omitempty" json:"response_headers"`
|
||||
|
||||
// tcpmux
|
||||
Multiplexer string `toml:"multiplexer,omitempty" json:"multiplexer"`
|
||||
|
||||
// stcp、sudp、xtcp
|
||||
SecretKey string `toml:"secretKey,omitempty" json:"secret_key"`
|
||||
AllowUsers []string `toml:"allowUsers,omitempty" json:"allow_users"`
|
||||
|
||||
Transport *ProxyTransport `toml:"transport,omitempty" json:"transport"`
|
||||
LoadBalancer *LoadBalancer `toml:"loadBalancer,omitempty" json:"load_balancer"`
|
||||
HealthCheck *HealthCheck `toml:"healthCheck,omitempty" json:"health_check"`
|
||||
Plugin *Plugin `toml:"plugin,omitempty" json:"plugin"`
|
||||
|
||||
Metadatas map[string]string `toml:"metadatas,omitempty" json:"metadatas"`
|
||||
Annotations map[string]string `toml:"annotations,omitempty" json:"annotations"`
|
||||
}
|
||||
|
||||
// Visitor 访问者配置,对应 stcp、sudp、xtcp 的客户端一侧
|
||||
type Visitor struct {
|
||||
Name string `toml:"name" form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
Type string `toml:"type" json:"type" validate:"required && in:stcp,sudp,xtcp"`
|
||||
Enabled *bool `toml:"enabled,omitempty" json:"enabled"`
|
||||
|
||||
ServerUser string `toml:"serverUser,omitempty" json:"server_user"`
|
||||
ServerName string `toml:"serverName,omitempty" json:"server_name"`
|
||||
SecretKey string `toml:"secretKey,omitempty" json:"secret_key"`
|
||||
BindAddr string `toml:"bindAddr,omitempty" json:"bind_addr"`
|
||||
BindPort int `toml:"bindPort,omitempty" json:"bind_port"`
|
||||
|
||||
// xtcp
|
||||
Protocol string `toml:"protocol,omitempty" json:"protocol"`
|
||||
KeepTunnelOpen bool `toml:"keepTunnelOpen,omitempty" json:"keep_tunnel_open"`
|
||||
MaxRetriesAnHour int `toml:"maxRetriesAnHour,omitempty" json:"max_retries_an_hour"`
|
||||
MinRetryInterval int `toml:"minRetryInterval,omitempty" json:"min_retry_interval"`
|
||||
FallbackTo string `toml:"fallbackTo,omitempty" json:"fallback_to"`
|
||||
FallbackTimeoutMs int `toml:"fallbackTimeoutMs,omitempty" json:"fallback_timeout_ms"`
|
||||
|
||||
Transport *VisitorTransport `toml:"transport,omitempty" json:"transport"`
|
||||
}
|
||||
|
||||
type ProxyTransport struct {
|
||||
UseEncryption bool `toml:"useEncryption,omitempty" json:"use_encryption"`
|
||||
UseCompression bool `toml:"useCompression,omitempty" json:"use_compression"`
|
||||
BandwidthLimit string `toml:"bandwidthLimit,omitempty" json:"bandwidth_limit"`
|
||||
BandwidthLimitMode string `toml:"bandwidthLimitMode,omitempty" json:"bandwidth_limit_mode"`
|
||||
ProxyProtocolVersion string `toml:"proxyProtocolVersion,omitempty" json:"proxy_protocol_version"`
|
||||
}
|
||||
|
||||
type VisitorTransport struct {
|
||||
UseEncryption bool `toml:"useEncryption,omitempty" json:"use_encryption"`
|
||||
UseCompression bool `toml:"useCompression,omitempty" json:"use_compression"`
|
||||
}
|
||||
|
||||
type LoadBalancer struct {
|
||||
Group string `toml:"group,omitempty" json:"group"`
|
||||
GroupKey string `toml:"groupKey,omitempty" json:"group_key"`
|
||||
}
|
||||
|
||||
type HealthCheck struct {
|
||||
Type string `toml:"type" json:"type"`
|
||||
TimeoutSeconds int `toml:"timeoutSeconds,omitempty" json:"timeout_seconds"`
|
||||
MaxFailed int `toml:"maxFailed,omitempty" json:"max_failed"`
|
||||
IntervalSeconds int `toml:"intervalSeconds,omitempty" json:"interval_seconds"`
|
||||
Path string `toml:"path,omitempty" json:"path"`
|
||||
}
|
||||
|
||||
type HeaderOperations struct {
|
||||
Set map[string]string `toml:"set,omitempty" json:"set"`
|
||||
}
|
||||
|
||||
// Plugin 客户端插件配置,各插件字段的并集
|
||||
type Plugin struct {
|
||||
Type string `toml:"type" json:"type"`
|
||||
|
||||
// unix_domain_socket
|
||||
UnixPath string `toml:"unixPath,omitempty" json:"unix_path"`
|
||||
|
||||
// http_proxy、static_file
|
||||
HTTPUser string `toml:"httpUser,omitempty" json:"http_user"`
|
||||
HTTPPassword string `toml:"httpPassword,omitempty" json:"http_password"`
|
||||
|
||||
// socks5
|
||||
Username string `toml:"username,omitempty" json:"username"`
|
||||
Password string `toml:"password,omitempty" json:"password"`
|
||||
|
||||
// static_file
|
||||
LocalPath string `toml:"localPath,omitempty" json:"local_path"`
|
||||
StripPrefix string `toml:"stripPrefix,omitempty" json:"strip_prefix"`
|
||||
|
||||
// https2http、https2https、http2https、http2http、tls2raw
|
||||
LocalAddr string `toml:"localAddr,omitempty" json:"local_addr"`
|
||||
HostHeaderRewrite string `toml:"hostHeaderRewrite,omitempty" json:"host_header_rewrite"`
|
||||
CrtPath string `toml:"crtPath,omitempty" json:"crt_path"`
|
||||
KeyPath string `toml:"keyPath,omitempty" json:"key_path"`
|
||||
}
|
||||
|
||||
// ServerTune frps 可视化参数
|
||||
type ServerTune struct {
|
||||
BindAddr string `form:"bind_addr" json:"bind_addr"`
|
||||
BindPort string `form:"bind_port" json:"bind_port"`
|
||||
KCPBindPort string `form:"kcp_bind_port" json:"kcp_bind_port"`
|
||||
QUICBindPort string `form:"quic_bind_port" json:"quic_bind_port"`
|
||||
ProxyBindAddr string `form:"proxy_bind_addr" json:"proxy_bind_addr"`
|
||||
SubDomainHost string `form:"sub_domain_host" json:"sub_domain_host"`
|
||||
MaxPortsPerClient string `form:"max_ports_per_client" json:"max_ports_per_client"`
|
||||
UDPPacketSize string `form:"udp_packet_size" json:"udp_packet_size"`
|
||||
DetailedErrorsToClient string `form:"detailed_errors_to_client" json:"detailed_errors_to_client"`
|
||||
|
||||
AuthMethod string `form:"auth_method" json:"auth_method"`
|
||||
AuthToken string `form:"auth_token" json:"auth_token"`
|
||||
|
||||
VhostHTTPPort string `form:"vhost_http_port" json:"vhost_http_port"`
|
||||
VhostHTTPSPort string `form:"vhost_https_port" json:"vhost_https_port"`
|
||||
VhostHTTPTimeout string `form:"vhost_http_timeout" json:"vhost_http_timeout"`
|
||||
TCPMuxHTTPConnectPort string `form:"tcpmux_http_connect_port" json:"tcpmux_http_connect_port"`
|
||||
Custom404Page string `form:"custom_404_page" json:"custom_404_page"`
|
||||
|
||||
WebServerAddr string `form:"web_server_addr" json:"web_server_addr"`
|
||||
WebServerPort string `form:"web_server_port" json:"web_server_port"`
|
||||
WebServerUser string `form:"web_server_user" json:"web_server_user"`
|
||||
WebServerPassword string `form:"web_server_password" json:"web_server_password"`
|
||||
WebServerPprofEnable string `form:"web_server_pprof_enable" json:"web_server_pprof_enable"`
|
||||
EnablePrometheus string `form:"enable_prometheus" json:"enable_prometheus"`
|
||||
|
||||
TransportMaxPoolCount string `form:"transport_max_pool_count" json:"transport_max_pool_count"`
|
||||
TransportTCPMux string `form:"transport_tcp_mux" json:"transport_tcp_mux"`
|
||||
TransportTCPMuxKeepaliveInterval string `form:"transport_tcp_mux_keepalive_interval" json:"transport_tcp_mux_keepalive_interval"`
|
||||
TransportTCPKeepalive string `form:"transport_tcp_keepalive" json:"transport_tcp_keepalive"`
|
||||
TransportHeartbeatTimeout string `form:"transport_heartbeat_timeout" json:"transport_heartbeat_timeout"`
|
||||
TransportTLSForce string `form:"transport_tls_force" json:"transport_tls_force"`
|
||||
TransportTLSCertFile string `form:"transport_tls_cert_file" json:"transport_tls_cert_file"`
|
||||
TransportTLSKeyFile string `form:"transport_tls_key_file" json:"transport_tls_key_file"`
|
||||
TransportTLSTrustedCaFile string `form:"transport_tls_trusted_ca_file" json:"transport_tls_trusted_ca_file"`
|
||||
|
||||
LogTo string `form:"log_to" json:"log_to"`
|
||||
LogLevel string `form:"log_level" json:"log_level"`
|
||||
LogMaxDays string `form:"log_max_days" json:"log_max_days"`
|
||||
LogDisablePrintColor string `form:"log_disable_print_color" json:"log_disable_print_color"`
|
||||
}
|
||||
|
||||
// ClientTune frpc 可视化公共参数
|
||||
type ClientTune struct {
|
||||
User string `form:"user" json:"user"`
|
||||
ServerAddr string `form:"server_addr" json:"server_addr"`
|
||||
ServerPort string `form:"server_port" json:"server_port"`
|
||||
LoginFailExit string `form:"login_fail_exit" json:"login_fail_exit"`
|
||||
NatHoleStunServer string `form:"nat_hole_stun_server" json:"nat_hole_stun_server"`
|
||||
DNSServer string `form:"dns_server" json:"dns_server"`
|
||||
UDPPacketSize string `form:"udp_packet_size" json:"udp_packet_size"`
|
||||
|
||||
AuthMethod string `form:"auth_method" json:"auth_method"`
|
||||
AuthToken string `form:"auth_token" json:"auth_token"`
|
||||
|
||||
TransportProtocol string `form:"transport_protocol" json:"transport_protocol"`
|
||||
TransportPoolCount string `form:"transport_pool_count" json:"transport_pool_count"`
|
||||
TransportTCPMux string `form:"transport_tcp_mux" json:"transport_tcp_mux"`
|
||||
TransportTCPMuxKeepaliveInterval string `form:"transport_tcp_mux_keepalive_interval" json:"transport_tcp_mux_keepalive_interval"`
|
||||
TransportDialServerTimeout string `form:"transport_dial_server_timeout" json:"transport_dial_server_timeout"`
|
||||
TransportDialServerKeepalive string `form:"transport_dial_server_keepalive" json:"transport_dial_server_keepalive"`
|
||||
TransportHeartbeatInterval string `form:"transport_heartbeat_interval" json:"transport_heartbeat_interval"`
|
||||
TransportHeartbeatTimeout string `form:"transport_heartbeat_timeout" json:"transport_heartbeat_timeout"`
|
||||
TransportConnectServerLocalIP string `form:"transport_connect_server_local_ip" json:"transport_connect_server_local_ip"`
|
||||
TransportProxyURL string `form:"transport_proxy_url" json:"transport_proxy_url"`
|
||||
TransportTLSEnable string `form:"transport_tls_enable" json:"transport_tls_enable"`
|
||||
TransportTLSCertFile string `form:"transport_tls_cert_file" json:"transport_tls_cert_file"`
|
||||
TransportTLSKeyFile string `form:"transport_tls_key_file" json:"transport_tls_key_file"`
|
||||
TransportTLSTrustedCaFile string `form:"transport_tls_trusted_ca_file" json:"transport_tls_trusted_ca_file"`
|
||||
TransportTLSServerName string `form:"transport_tls_server_name" json:"transport_tls_server_name"`
|
||||
|
||||
WebServerAddr string `form:"web_server_addr" json:"web_server_addr"`
|
||||
WebServerPort string `form:"web_server_port" json:"web_server_port"`
|
||||
WebServerUser string `form:"web_server_user" json:"web_server_user"`
|
||||
WebServerPassword string `form:"web_server_password" json:"web_server_password"`
|
||||
WebServerPprofEnable string `form:"web_server_pprof_enable" json:"web_server_pprof_enable"`
|
||||
|
||||
LogTo string `form:"log_to" json:"log_to"`
|
||||
LogLevel string `form:"log_level" json:"log_level"`
|
||||
LogMaxDays string `form:"log_max_days" json:"log_max_days"`
|
||||
LogDisablePrintColor string `form:"log_disable_print_color" json:"log_disable_print_color"`
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -23,27 +22,15 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("gitea")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(fmt.Sprintf("%s/server/gitea/app.ini", app.Root))
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, app.Root+"/server/gitea/app.ini")
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/gitea/app.ini", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("gitea"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, app.Root+"/server/gitea/app.ini", "gitea")
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package gitea
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
+64
-183
@@ -4,15 +4,17 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -40,6 +42,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Delete("/datasources/{name}", s.DeleteDataSource)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("grafana")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("grafana")
|
||||
if err != nil {
|
||||
@@ -53,7 +60,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 从 defaults.ini 获取端口
|
||||
config, _ := io.Read(s.configPath())
|
||||
port := s.getINIValue(config, "server", "http_port")
|
||||
port := confval.SectionINI.GetIn(config, "server", "http_port")
|
||||
if port == "" {
|
||||
port = "3000"
|
||||
}
|
||||
@@ -61,7 +68,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/api/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
@@ -86,29 +93,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("grafana"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "grafana")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Grafana 配置调整参数
|
||||
@@ -116,7 +105,7 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
get := func(section, key string) string {
|
||||
return s.getINIValue(config, section, key)
|
||||
return confval.SectionINI.GetIn(config, section, key)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
@@ -162,31 +151,31 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
// [server]
|
||||
config = s.setINIValue(config, "server", "http_port", req.HTTPPort)
|
||||
config = s.setINIValue(config, "server", "domain", req.Domain)
|
||||
config = s.setINIValue(config, "server", "root_url", req.RootURL)
|
||||
config = s.setINIValue(config, "server", "protocol", req.Protocol)
|
||||
config = confval.SectionINI.SetIn(config, "server", "http_port", req.HTTPPort)
|
||||
config = confval.SectionINI.SetIn(config, "server", "domain", req.Domain)
|
||||
config = confval.SectionINI.SetIn(config, "server", "root_url", req.RootURL)
|
||||
config = confval.SectionINI.SetIn(config, "server", "protocol", req.Protocol)
|
||||
// [database]
|
||||
config = s.setINIValue(config, "database", "type", req.DBType)
|
||||
config = s.setINIValue(config, "database", "host", req.DBHost)
|
||||
config = s.setINIValue(config, "database", "name", req.DBName)
|
||||
config = s.setINIValue(config, "database", "user", req.DBUser)
|
||||
config = s.setINIValue(config, "database", "password", req.DBPassword)
|
||||
config = confval.SectionINI.SetIn(config, "database", "type", req.DBType)
|
||||
config = confval.SectionINI.SetIn(config, "database", "host", req.DBHost)
|
||||
config = confval.SectionINI.SetIn(config, "database", "name", req.DBName)
|
||||
config = confval.SectionINI.SetIn(config, "database", "user", req.DBUser)
|
||||
config = confval.SectionINI.SetIn(config, "database", "password", req.DBPassword)
|
||||
// [security]
|
||||
config = s.setINIValue(config, "security", "admin_user", req.AdminUser)
|
||||
config = s.setINIValue(config, "security", "admin_password", req.AdminPassword)
|
||||
config = confval.SectionINI.SetIn(config, "security", "admin_user", req.AdminUser)
|
||||
config = confval.SectionINI.SetIn(config, "security", "admin_password", req.AdminPassword)
|
||||
// [users]
|
||||
config = s.setINIValue(config, "users", "allow_sign_up", req.AllowSignUp)
|
||||
config = s.setINIValue(config, "users", "auto_assign_org_role", req.AutoAssignOrgRole)
|
||||
config = confval.SectionINI.SetIn(config, "users", "allow_sign_up", req.AllowSignUp)
|
||||
config = confval.SectionINI.SetIn(config, "users", "auto_assign_org_role", req.AutoAssignOrgRole)
|
||||
// [smtp]
|
||||
config = s.setINIValue(config, "smtp", "enabled", req.SMTPEnabled)
|
||||
config = s.setINIValue(config, "smtp", "host", req.SMTPHost)
|
||||
config = s.setINIValue(config, "smtp", "user", req.SMTPUser)
|
||||
config = s.setINIValue(config, "smtp", "password", req.SMTPPassword)
|
||||
config = s.setINIValue(config, "smtp", "from_address", req.SMTPFromAddress)
|
||||
config = confval.SectionINI.SetIn(config, "smtp", "enabled", req.SMTPEnabled)
|
||||
config = confval.SectionINI.SetIn(config, "smtp", "host", req.SMTPHost)
|
||||
config = confval.SectionINI.SetIn(config, "smtp", "user", req.SMTPUser)
|
||||
config = confval.SectionINI.SetIn(config, "smtp", "password", req.SMTPPassword)
|
||||
config = confval.SectionINI.SetIn(config, "smtp", "from_address", req.SMTPFromAddress)
|
||||
// [log]
|
||||
config = s.setINIValue(config, "log", "mode", req.LogMode)
|
||||
config = s.setINIValue(config, "log", "level", req.LogLevel)
|
||||
config = confval.SectionINI.SetIn(config, "log", "mode", req.LogMode)
|
||||
config = confval.SectionINI.SetIn(config, "log", "level", req.LogLevel)
|
||||
|
||||
if err = io.Write(s.configPath(), config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -216,11 +205,12 @@ func (s *App) CreateDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
cfg := s.readDatasources()
|
||||
|
||||
for _, item := range s.getDatasourceList(cfg) {
|
||||
if ds, ok := item.(map[string]any); ok && ds["name"] == req.Name {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("datasource %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
if lo.ContainsBy(s.getDatasourceList(cfg), func(item any) bool {
|
||||
ds, ok := item.(map[string]any)
|
||||
return ok && ds["name"] == req.Name
|
||||
}) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("datasource %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if req.IsDefault {
|
||||
@@ -251,33 +241,31 @@ func (s *App) UpdateDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := s.readDatasources()
|
||||
list := s.getDatasourceList(cfg)
|
||||
|
||||
found := false
|
||||
for i, item := range list {
|
||||
if ds, ok := item.(map[string]any); ok && ds["name"] == oldName {
|
||||
found = true
|
||||
newDs := s.buildDatasourceMap(req)
|
||||
// 密码为空时保留原有 secureJsonData
|
||||
if req.Password == "" {
|
||||
if sec, exists := ds["secureJsonData"]; exists {
|
||||
newDs["secureJsonData"] = sec
|
||||
}
|
||||
}
|
||||
list[i] = newDs
|
||||
break
|
||||
}
|
||||
}
|
||||
_, idx, found := lo.FindIndexOf(list, func(item any) bool {
|
||||
ds, ok := item.(map[string]any)
|
||||
return ok && ds["name"] == oldName
|
||||
})
|
||||
if !found {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("datasource %s not found", oldName))
|
||||
return
|
||||
}
|
||||
ds, _ := list[idx].(map[string]any)
|
||||
newDs := s.buildDatasourceMap(req)
|
||||
// 密码为空时保留原有 secureJsonData
|
||||
if req.Password == "" {
|
||||
if sec, exists := ds["secureJsonData"]; exists {
|
||||
newDs["secureJsonData"] = sec
|
||||
}
|
||||
}
|
||||
list[idx] = newDs
|
||||
|
||||
if req.IsDefault {
|
||||
s.clearDefault(cfg)
|
||||
for _, item := range list {
|
||||
lo.ForEach(list, func(item any, _ int) {
|
||||
if ds, ok := item.(map[string]any); ok && ds["name"] == req.Name {
|
||||
ds["isDefault"] = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if oldName != req.Name {
|
||||
@@ -300,15 +288,11 @@ func (s *App) DeleteDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := s.readDatasources()
|
||||
list := s.getDatasourceList(cfg)
|
||||
|
||||
found := false
|
||||
newList := make([]any, 0, len(list))
|
||||
for _, item := range list {
|
||||
if ds, ok := item.(map[string]any); ok && ds["name"] == name {
|
||||
found = true
|
||||
continue
|
||||
}
|
||||
newList = append(newList, item)
|
||||
}
|
||||
newList := lo.Filter(list, func(item any, _ int) bool {
|
||||
ds, ok := item.(map[string]any)
|
||||
return !ok || ds["name"] != name
|
||||
})
|
||||
found := len(newList) < len(list)
|
||||
if !found {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("datasource %s not found", name))
|
||||
return
|
||||
@@ -327,115 +311,12 @@ func (s *App) DeleteDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// configPath 返回 Grafana 主配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/grafana/conf/defaults.ini", app.Root)
|
||||
}
|
||||
|
||||
// getINIValue 从 INI 配置中获取指定 section 下的 key 值
|
||||
func (s *App) getINIValue(content string, section string, key string) string {
|
||||
currentSection := ""
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, ";") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||
currentSection = strings.TrimSpace(trimmed[1 : len(trimmed)-1])
|
||||
continue
|
||||
}
|
||||
if currentSection != section {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) == 2 && strings.TrimSpace(parts[0]) == key {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setINIValue 在 INI 配置中设置指定 section 下的 key 值
|
||||
func (s *App) setINIValue(content string, section string, key string, value string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
currentSection := ""
|
||||
found := false
|
||||
lastSectionLine := -1 // 目标 section 的最后一行索引
|
||||
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// 检测 section 头
|
||||
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||
// 如果离开目标 section 且未找到 key,在 section 末尾插入
|
||||
if currentSection == section && !found && lastSectionLine >= 0 {
|
||||
found = true
|
||||
if value != "" {
|
||||
// 在 section 末尾插入新行
|
||||
insertIdx := lastSectionLine + 1
|
||||
newLine := key + " = " + value
|
||||
result = append(result[:insertIdx+1], append([]string{newLine}, result[insertIdx+1:]...)...)
|
||||
}
|
||||
}
|
||||
currentSection = strings.TrimSpace(trimmed[1 : len(trimmed)-1])
|
||||
}
|
||||
|
||||
if currentSection == section {
|
||||
lastSectionLine = len(result)
|
||||
}
|
||||
|
||||
// 在目标 section 内匹配 key
|
||||
if currentSection == section && !found {
|
||||
checkLine := trimmed
|
||||
commented := false
|
||||
if strings.HasPrefix(checkLine, ";") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
commented = true
|
||||
} else if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
commented = true
|
||||
}
|
||||
parts := strings.SplitN(checkLine, "=", 2)
|
||||
if len(parts) == 2 && strings.TrimSpace(parts[0]) == key {
|
||||
found = true
|
||||
if value == "" {
|
||||
// 值为空时注释掉
|
||||
if !commented {
|
||||
result = append(result, ";"+line)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
} else {
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
_ = i
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, line)
|
||||
}
|
||||
|
||||
// 如果在最后一个 section 中未找到 key
|
||||
if currentSection == section && !found {
|
||||
found = true
|
||||
if value != "" {
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
}
|
||||
|
||||
// section 不存在,在文件末尾追加
|
||||
if !found && value != "" {
|
||||
result = append(result, "")
|
||||
result = append(result, "["+section+"]")
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
return app.Root + "/server/grafana/conf/defaults.ini"
|
||||
}
|
||||
|
||||
// datasourcePath 返回 provisioning 数据源文件路径
|
||||
func (s *App) datasourcePath() string {
|
||||
return fmt.Sprintf("%s/server/grafana/conf/provisioning/datasources/panel.yml", app.Root)
|
||||
return app.Root + "/server/grafana/conf/provisioning/datasources/panel.yml"
|
||||
}
|
||||
|
||||
// readDatasources 读取 provisioning 文件
|
||||
@@ -492,7 +373,7 @@ func (s *App) buildDatasourceMap(req *DataSource) map[string]any {
|
||||
ds["access"] = "proxy"
|
||||
}
|
||||
switch req.Type {
|
||||
case "mysql", "postgres", "influxdb":
|
||||
case "mysql", "postgres", "influxdb", "mssql":
|
||||
if req.Database != "" {
|
||||
ds["database"] = req.Database
|
||||
}
|
||||
@@ -508,11 +389,11 @@ func (s *App) buildDatasourceMap(req *DataSource) map[string]any {
|
||||
|
||||
// clearDefault 清除所有数据源的默认标记
|
||||
func (s *App) clearDefault(cfg map[string]any) {
|
||||
for _, item := range s.getDatasourceList(cfg) {
|
||||
lo.ForEach(s.getDatasourceList(cfg), func(item any, _ int) {
|
||||
if ds, ok := item.(map[string]any); ok {
|
||||
ds["isDefault"] = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// addDeleteEntry 向 deleteDatasources 添加条目
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package grafana
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Grafana 配置调整
|
||||
type ConfigTune struct {
|
||||
// [server]
|
||||
@@ -37,9 +33,9 @@ type ConfigTune struct {
|
||||
// DataSource 数据源
|
||||
type DataSource struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Type string `form:"type" json:"type" validate:"required|in:prometheus,mysql,postgres,influxdb,loki,elasticsearch"`
|
||||
Type string `form:"type" json:"type" validate:"required && in:prometheus,mysql,postgres,influxdb,loki,elasticsearch,tempo,jaeger,zipkin,graphite,alertmanager,opentsdb,mssql,testdata,grafana-pyroscope-datasource,grafana-opensearch-datasource"`
|
||||
URL string `form:"url" json:"url" validate:"required"`
|
||||
Access string `form:"access" json:"access"`
|
||||
Access string `form:"access" json:"access" validate:"in:proxy,direct"`
|
||||
IsDefault bool `form:"is_default" json:"is_default"`
|
||||
// 数据库类型专用
|
||||
Database string `form:"database" json:"database"`
|
||||
|
||||
+29
-98
@@ -4,12 +4,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -32,6 +33,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("kafka")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("kafka")
|
||||
if err != nil {
|
||||
@@ -46,40 +52,23 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Node ID"), Value: s.getPropertiesValue(config, "node.id")},
|
||||
{Name: s.t.Get("Listeners"), Value: s.getPropertiesValue(config, "listeners")},
|
||||
{Name: s.t.Get("Log Dirs"), Value: s.getPropertiesValue(config, "log.dirs")},
|
||||
{Name: s.t.Get("Num Partitions"), Value: s.getPropertiesValue(config, "num.partitions")},
|
||||
{Name: s.t.Get("Log Retention Hours"), Value: s.getPropertiesValue(config, "log.retention.hours")},
|
||||
{Name: s.t.Get("Log Segment Bytes"), Value: s.getPropertiesValue(config, "log.segment.bytes")},
|
||||
{Name: s.t.Get("Node ID"), Value: confval.Properties.Get(config, "node.id")},
|
||||
{Name: s.t.Get("Listeners"), Value: confval.Properties.Get(config, "listeners")},
|
||||
{Name: s.t.Get("Log Dirs"), Value: confval.Properties.Get(config, "log.dirs")},
|
||||
{Name: s.t.Get("Num Partitions"), Value: confval.Properties.Get(config, "num.partitions")},
|
||||
{Name: s.t.Get("Log Retention Hours"), Value: confval.Properties.Get(config, "log.retention.hours")},
|
||||
{Name: s.t.Get("Log Segment Bytes"), Value: confval.Properties.Get(config, "log.segment.bytes")},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("kafka"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "kafka")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Kafka 配置调整参数
|
||||
@@ -90,12 +79,12 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
heapInit, heapMax := s.parseHeapEnv(heapRaw)
|
||||
|
||||
tune := ConfigTune{
|
||||
NodeID: s.getPropertiesValue(config, "node.id"),
|
||||
Listeners: s.getPropertiesValue(config, "listeners"),
|
||||
LogDirs: s.getPropertiesValue(config, "log.dirs"),
|
||||
NumPartitions: s.getPropertiesValue(config, "num.partitions"),
|
||||
RetentionHours: s.getPropertiesValue(config, "log.retention.hours"),
|
||||
LogSegmentBytes: s.getPropertiesValue(config, "log.segment.bytes"),
|
||||
NodeID: confval.Properties.Get(config, "node.id"),
|
||||
Listeners: confval.Properties.Get(config, "listeners"),
|
||||
LogDirs: confval.Properties.Get(config, "log.dirs"),
|
||||
NumPartitions: confval.Properties.Get(config, "num.partitions"),
|
||||
RetentionHours: confval.Properties.Get(config, "log.retention.hours"),
|
||||
LogSegmentBytes: confval.Properties.Get(config, "log.segment.bytes"),
|
||||
HeapInitSize: heapInit,
|
||||
HeapMaxSize: heapMax,
|
||||
}
|
||||
@@ -113,12 +102,12 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
config = s.setPropertiesValue(config, "node.id", req.NodeID)
|
||||
config = s.setPropertiesValue(config, "listeners", req.Listeners)
|
||||
config = s.setPropertiesValue(config, "log.dirs", req.LogDirs)
|
||||
config = s.setPropertiesValue(config, "num.partitions", req.NumPartitions)
|
||||
config = s.setPropertiesValue(config, "log.retention.hours", req.RetentionHours)
|
||||
config = s.setPropertiesValue(config, "log.segment.bytes", req.LogSegmentBytes)
|
||||
config = confval.Properties.Set(config, "node.id", req.NodeID)
|
||||
config = confval.Properties.Set(config, "listeners", req.Listeners)
|
||||
config = confval.Properties.Set(config, "log.dirs", req.LogDirs)
|
||||
config = confval.Properties.Set(config, "num.partitions", req.NumPartitions)
|
||||
config = confval.Properties.Set(config, "log.retention.hours", req.RetentionHours)
|
||||
config = confval.Properties.Set(config, "log.segment.bytes", req.LogSegmentBytes)
|
||||
|
||||
if err = io.Write(s.configPath(), config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -145,70 +134,12 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/kafka/config/server.properties", app.Root)
|
||||
return app.Root + "/server/kafka/config/server.properties"
|
||||
}
|
||||
|
||||
// heapEnvPath 返回 JVM 堆内存配置文件路径
|
||||
func (s *App) heapEnvPath() string {
|
||||
return fmt.Sprintf("%s/server/kafka/config/heap.env", app.Root)
|
||||
}
|
||||
|
||||
// getPropertiesValue 从 properties 内容中获取指定键的值
|
||||
func (s *App) getPropertiesValue(content string, key string) string {
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(trimmed, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setPropertiesValue 在 properties 内容中设置指定键的值
|
||||
func (s *App) setPropertiesValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
k, _, ok := strings.Cut(checkLine, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+trimmed)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+"="+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+"="+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
return app.Root + "/server/kafka/config/heap.env"
|
||||
}
|
||||
|
||||
// parseHeapEnv 从 heap.env 中提取堆内存配置
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package kafka
|
||||
|
||||
// UpdateConfig Kafka 配置更新
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Kafka 配置调整
|
||||
type ConfigTune struct {
|
||||
// Broker
|
||||
|
||||
@@ -2,22 +2,24 @@ package mariadb
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
mysql *mysql.App
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(mysqlApp *mysql.App) *App {
|
||||
return &App{
|
||||
mysql: mysql.NewApp(t, setting, databaseServer),
|
||||
mysql: mysqlApp,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
s.mysql.Route(r)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
return s.mysql.Status()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -34,6 +35,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("memcached")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("memcached")
|
||||
if err != nil {
|
||||
@@ -85,33 +91,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/systemd/system/memcached.service")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, "/etc/systemd/system/memcached.service")
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/systemd/system/memcached.service", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("memcached"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, "/etc/systemd/system/memcached.service", "memcached")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Memcached 配置调整参数
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package memcached
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Memcached 配置调整
|
||||
type ConfigTune struct {
|
||||
Port string `form:"port" json:"port"`
|
||||
UDPPort string `form:"udp_port" json:"udp_port"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
UDPPort string `form:"udp_port" json:"udp_port" validate:"number && min:1 && max:65535"`
|
||||
ListenAddress string `form:"listen_address" json:"listen_address"`
|
||||
Memory string `form:"memory" json:"memory"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections"`
|
||||
Threads string `form:"threads" json:"threads"`
|
||||
Memory string `form:"memory" json:"memory" validate:"number && min:1"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections" validate:"number && min:1"`
|
||||
Threads string `form:"threads" json:"threads" validate:"number && min:1"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -21,6 +22,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/env", s.UpdateEnv)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("minio")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetEnv(w http.ResponseWriter, r *http.Request) {
|
||||
env, _ := io.Read("/etc/default/minio")
|
||||
service.Success(w, env)
|
||||
|
||||
@@ -2,7 +2,6 @@ package mongodb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"go.yaml.in/yaml/v4"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
@@ -26,11 +26,11 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/admin_password", s.SetAdminPassword)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("mongod")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// Load 获取 MongoDB 运行状态
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, _ := systemctl.Status("mongod")
|
||||
@@ -102,29 +107,12 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("mongod"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "mongod")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取配置调整参数
|
||||
@@ -235,7 +223,7 @@ func (s *App) SetAdminPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/mongodb/mongod.conf", app.Root)
|
||||
return app.Root + "/server/mongodb/mongod.conf"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取嵌套 YAML 值,支持 dot notation
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
package mongodb
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// SetAdminPassword 设置 admin 密码
|
||||
type SetAdminPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
// ConfigTune MongoDB 配置调整
|
||||
type ConfigTune struct {
|
||||
// 存储
|
||||
DbPath string `form:"db_path" json:"db_path"`
|
||||
DbPath string `form:"db_path" json:"db_path" validate:"unix_path"`
|
||||
CacheSizeGB string `form:"cache_size_gb" json:"cache_size_gb"`
|
||||
// 网络
|
||||
Port string `form:"port" json:"port"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
BindIp string `form:"bind_ip" json:"bind_ip"`
|
||||
// 日志
|
||||
SystemLogPath string `form:"system_log_path" json:"system_log_path"`
|
||||
SystemLogPath string `form:"system_log_path" json:"system_log_path" validate:"unix_path"`
|
||||
// 安全
|
||||
Authorization string `form:"authorization" json:"authorization"`
|
||||
Authorization string `form:"authorization" json:"authorization" validate:"in:enabled,disabled"`
|
||||
}
|
||||
|
||||
+635
-203
@@ -1,10 +1,12 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
@@ -26,13 +29,15 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
settingRepo biz.SettingRepo
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo, taskRepo biz.TaskRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
taskRepo: taskRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,45 +45,30 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Post("/clear_log", s.ClearLog)
|
||||
r.Get("/slow_log", s.SlowLog)
|
||||
r.Post("/clear_slow_log", s.ClearSlowLog)
|
||||
r.Get("/root_password", s.GetRootPassword)
|
||||
r.Post("/root_password", s.SetRootPassword)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
// 性能
|
||||
r.Get("/processes", s.ProcessList)
|
||||
r.Post("/processes/{id}/kill", s.KillProcess)
|
||||
r.Get("/transactions", s.TransactionList)
|
||||
r.Get("/top_sql", s.TopSQL)
|
||||
r.Post("/top_sql/enable", s.EnableTopSQL)
|
||||
r.Post("/top_sql/reset", s.ResetTopSQL)
|
||||
// 维护
|
||||
r.Get("/databases", s.DatabaseList)
|
||||
r.Get("/tables", s.TableList)
|
||||
r.Post("/maintenance", s.RunMaintenance)
|
||||
r.Get("/binlogs", s.BinlogList)
|
||||
r.Post("/binlogs/purge", s.PurgeBinlog)
|
||||
r.Get("/replication", s.ReplicationStatus)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// UpdateConfig 保存配置
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("mysqld"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to restart MySQL: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("mysqld")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
@@ -95,19 +85,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set MYSQL_PWD env: %v", err))
|
||||
return
|
||||
}
|
||||
raw, err := shell.Execf(`mysqladmin -u root extended-status`)
|
||||
raw, err := shell.ExecfWithEnv([]string{"MYSQL_PWD=" + rootPassword}, `mysqladmin -u root extended-status`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get MySQL status: %v", err))
|
||||
return
|
||||
}
|
||||
if err = os.Unsetenv("MYSQL_PWD"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to unset MYSQL_PWD env: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
var load []map[string]string
|
||||
expressions := []struct {
|
||||
@@ -194,29 +176,41 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, load)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *App) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if err := systemctl.LogClear("mysqld"); err != nil {
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// UpdateConfig 保存配置
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("mysqld"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to restart MySQL: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (s *App) SlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, fmt.Sprintf("%s/server/mysql/mysql-slow.log", app.Root))
|
||||
}
|
||||
|
||||
// ClearSlowLog 清空慢查询日志
|
||||
func (s *App) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf("cat /dev/null > %s/server/mysql/mysql-slow.log", app.Root); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
service.Success(w, app.Root+"/server/mysql/mysql-slow.log")
|
||||
}
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
@@ -239,10 +233,10 @@ func (s *App) SetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
oldRootPassword, _ := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
mysql, err := db.NewMySQL("root", oldRootPassword, s.getSock(), "unix")
|
||||
mysql, err := db.NewMySQL(r.Context(), "root", oldRootPassword, db.MySQLSocket(app.Root), "unix")
|
||||
if err != nil {
|
||||
// 尝试安全模式直接改密
|
||||
if err = db.MySQLResetRootPassword(req.Password); err != nil {
|
||||
if err = db.MySQLResetRootPassword(req.Password, app.Root); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -264,30 +258,6 @@ func (s *App) SetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) getSock() string {
|
||||
if io.Exists("/tmp/mysql.sock") {
|
||||
return "/tmp/mysql.sock"
|
||||
}
|
||||
if io.Exists(app.Root + "/server/mysql/config/my.cnf") {
|
||||
config, _ := io.Read(app.Root + "/server/mysql/config/my.cnf")
|
||||
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) > 2 {
|
||||
return matches[2]
|
||||
}
|
||||
}
|
||||
if io.Exists("/etc/my.cnf") {
|
||||
config, _ := io.Read("/etc/my.cnf")
|
||||
re := regexp.MustCompile(`socket\s*=\s*(['"]?)([^'"]+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) > 2 {
|
||||
return matches[2]
|
||||
}
|
||||
}
|
||||
|
||||
return "/tmp/mysql.sock"
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 MySQL 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
@@ -298,35 +268,35 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tune := ConfigTune{
|
||||
// 常规设置
|
||||
Port: s.getINIValue(config, "port"),
|
||||
MaxConnections: s.getINIValue(config, "max_connections"),
|
||||
MaxConnectErrors: s.getINIValue(config, "max_connect_errors"),
|
||||
DefaultStorageEngine: s.getINIValue(config, "default_storage_engine"),
|
||||
TableOpenCache: s.getINIValue(config, "table_open_cache"),
|
||||
MaxAllowedPacket: s.getINIValue(config, "max_allowed_packet"),
|
||||
OpenFilesLimit: s.getINIValue(config, "open_files_limit"),
|
||||
Port: confval.INI.Get(config, "port"),
|
||||
MaxConnections: confval.INI.Get(config, "max_connections"),
|
||||
MaxConnectErrors: confval.INI.Get(config, "max_connect_errors"),
|
||||
DefaultStorageEngine: confval.INI.Get(config, "default_storage_engine"),
|
||||
TableOpenCache: confval.INI.Get(config, "table_open_cache"),
|
||||
MaxAllowedPacket: confval.INI.Get(config, "max_allowed_packet"),
|
||||
OpenFilesLimit: confval.INI.Get(config, "open_files_limit"),
|
||||
// 性能调整
|
||||
KeyBufferSize: s.getINIValue(config, "key_buffer_size"),
|
||||
SortBufferSize: s.getINIValue(config, "sort_buffer_size"),
|
||||
ReadBufferSize: s.getINIValue(config, "read_buffer_size"),
|
||||
ReadRndBufferSize: s.getINIValue(config, "read_rnd_buffer_size"),
|
||||
JoinBufferSize: s.getINIValue(config, "join_buffer_size"),
|
||||
ThreadCacheSize: s.getINIValue(config, "thread_cache_size"),
|
||||
ThreadStack: s.getINIValue(config, "thread_stack"),
|
||||
TmpTableSize: s.getINIValue(config, "tmp_table_size"),
|
||||
MaxHeapTableSize: s.getINIValue(config, "max_heap_table_size"),
|
||||
MyisamSortBufferSize: s.getINIValue(config, "myisam_sort_buffer_size"),
|
||||
KeyBufferSize: confval.INI.Get(config, "key_buffer_size"),
|
||||
SortBufferSize: confval.INI.Get(config, "sort_buffer_size"),
|
||||
ReadBufferSize: confval.INI.Get(config, "read_buffer_size"),
|
||||
ReadRndBufferSize: confval.INI.Get(config, "read_rnd_buffer_size"),
|
||||
JoinBufferSize: confval.INI.Get(config, "join_buffer_size"),
|
||||
ThreadCacheSize: confval.INI.Get(config, "thread_cache_size"),
|
||||
ThreadStack: confval.INI.Get(config, "thread_stack"),
|
||||
TmpTableSize: confval.INI.Get(config, "tmp_table_size"),
|
||||
MaxHeapTableSize: confval.INI.Get(config, "max_heap_table_size"),
|
||||
MyisamSortBufferSize: confval.INI.Get(config, "myisam_sort_buffer_size"),
|
||||
// InnoDB
|
||||
InnodbBufferPoolSize: s.getINIValue(config, "innodb_buffer_pool_size"),
|
||||
InnodbLogBufferSize: s.getINIValue(config, "innodb_log_buffer_size"),
|
||||
InnodbFlushLogAtTrxCommit: s.getINIValue(config, "innodb_flush_log_at_trx_commit"),
|
||||
InnodbLockWaitTimeout: s.getINIValue(config, "innodb_lock_wait_timeout"),
|
||||
InnodbMaxDirtyPagesPct: s.getINIValue(config, "innodb_max_dirty_pages_pct"),
|
||||
InnodbReadIoThreads: s.getINIValue(config, "innodb_read_io_threads"),
|
||||
InnodbWriteIoThreads: s.getINIValue(config, "innodb_write_io_threads"),
|
||||
InnodbBufferPoolSize: confval.INI.Get(config, "innodb_buffer_pool_size"),
|
||||
InnodbLogBufferSize: confval.INI.Get(config, "innodb_log_buffer_size"),
|
||||
InnodbFlushLogAtTrxCommit: confval.INI.Get(config, "innodb_flush_log_at_trx_commit"),
|
||||
InnodbLockWaitTimeout: confval.INI.Get(config, "innodb_lock_wait_timeout"),
|
||||
InnodbMaxDirtyPagesPct: confval.INI.Get(config, "innodb_max_dirty_pages_pct"),
|
||||
InnodbReadIoThreads: confval.INI.Get(config, "innodb_read_io_threads"),
|
||||
InnodbWriteIoThreads: confval.INI.Get(config, "innodb_write_io_threads"),
|
||||
// 日志
|
||||
SlowQueryLog: s.getINIValue(config, "slow_query_log"),
|
||||
LongQueryTime: s.getINIValue(config, "long_query_time"),
|
||||
SlowQueryLog: confval.INI.Get(config, "slow_query_log"),
|
||||
LongQueryTime: confval.INI.Get(config, "long_query_time"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
@@ -348,35 +318,35 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 更新常规设置
|
||||
config = s.setINIValue(config, "port", req.Port)
|
||||
config = s.setINIValue(config, "max_connections", req.MaxConnections)
|
||||
config = s.setINIValue(config, "max_connect_errors", req.MaxConnectErrors)
|
||||
config = s.setINIValue(config, "default_storage_engine", req.DefaultStorageEngine)
|
||||
config = s.setINIValue(config, "table_open_cache", req.TableOpenCache)
|
||||
config = s.setINIValue(config, "max_allowed_packet", req.MaxAllowedPacket)
|
||||
config = s.setINIValue(config, "open_files_limit", req.OpenFilesLimit)
|
||||
config = confval.INI.Set(config, "port", req.Port)
|
||||
config = confval.INI.Set(config, "max_connections", req.MaxConnections)
|
||||
config = confval.INI.Set(config, "max_connect_errors", req.MaxConnectErrors)
|
||||
config = confval.INI.Set(config, "default_storage_engine", req.DefaultStorageEngine)
|
||||
config = confval.INI.Set(config, "table_open_cache", req.TableOpenCache)
|
||||
config = confval.INI.Set(config, "max_allowed_packet", req.MaxAllowedPacket)
|
||||
config = confval.INI.Set(config, "open_files_limit", req.OpenFilesLimit)
|
||||
// 更新性能调整
|
||||
config = s.setINIValue(config, "key_buffer_size", req.KeyBufferSize)
|
||||
config = s.setINIValue(config, "sort_buffer_size", req.SortBufferSize)
|
||||
config = s.setINIValue(config, "read_buffer_size", req.ReadBufferSize)
|
||||
config = s.setINIValue(config, "read_rnd_buffer_size", req.ReadRndBufferSize)
|
||||
config = s.setINIValue(config, "join_buffer_size", req.JoinBufferSize)
|
||||
config = s.setINIValue(config, "thread_cache_size", req.ThreadCacheSize)
|
||||
config = s.setINIValue(config, "thread_stack", req.ThreadStack)
|
||||
config = s.setINIValue(config, "tmp_table_size", req.TmpTableSize)
|
||||
config = s.setINIValue(config, "max_heap_table_size", req.MaxHeapTableSize)
|
||||
config = s.setINIValue(config, "myisam_sort_buffer_size", req.MyisamSortBufferSize)
|
||||
config = confval.INI.Set(config, "key_buffer_size", req.KeyBufferSize)
|
||||
config = confval.INI.Set(config, "sort_buffer_size", req.SortBufferSize)
|
||||
config = confval.INI.Set(config, "read_buffer_size", req.ReadBufferSize)
|
||||
config = confval.INI.Set(config, "read_rnd_buffer_size", req.ReadRndBufferSize)
|
||||
config = confval.INI.Set(config, "join_buffer_size", req.JoinBufferSize)
|
||||
config = confval.INI.Set(config, "thread_cache_size", req.ThreadCacheSize)
|
||||
config = confval.INI.Set(config, "thread_stack", req.ThreadStack)
|
||||
config = confval.INI.Set(config, "tmp_table_size", req.TmpTableSize)
|
||||
config = confval.INI.Set(config, "max_heap_table_size", req.MaxHeapTableSize)
|
||||
config = confval.INI.Set(config, "myisam_sort_buffer_size", req.MyisamSortBufferSize)
|
||||
// 更新 InnoDB
|
||||
config = s.setINIValue(config, "innodb_buffer_pool_size", req.InnodbBufferPoolSize)
|
||||
config = s.setINIValue(config, "innodb_log_buffer_size", req.InnodbLogBufferSize)
|
||||
config = s.setINIValue(config, "innodb_flush_log_at_trx_commit", req.InnodbFlushLogAtTrxCommit)
|
||||
config = s.setINIValue(config, "innodb_lock_wait_timeout", req.InnodbLockWaitTimeout)
|
||||
config = s.setINIValue(config, "innodb_max_dirty_pages_pct", req.InnodbMaxDirtyPagesPct)
|
||||
config = s.setINIValue(config, "innodb_read_io_threads", req.InnodbReadIoThreads)
|
||||
config = s.setINIValue(config, "innodb_write_io_threads", req.InnodbWriteIoThreads)
|
||||
config = confval.INI.Set(config, "innodb_buffer_pool_size", req.InnodbBufferPoolSize)
|
||||
config = confval.INI.Set(config, "innodb_log_buffer_size", req.InnodbLogBufferSize)
|
||||
config = confval.INI.Set(config, "innodb_flush_log_at_trx_commit", req.InnodbFlushLogAtTrxCommit)
|
||||
config = confval.INI.Set(config, "innodb_lock_wait_timeout", req.InnodbLockWaitTimeout)
|
||||
config = confval.INI.Set(config, "innodb_max_dirty_pages_pct", req.InnodbMaxDirtyPagesPct)
|
||||
config = confval.INI.Set(config, "innodb_read_io_threads", req.InnodbReadIoThreads)
|
||||
config = confval.INI.Set(config, "innodb_write_io_threads", req.InnodbWriteIoThreads)
|
||||
// 更新日志
|
||||
config = s.setINIValue(config, "slow_query_log", req.SlowQueryLog)
|
||||
config = s.setINIValue(config, "long_query_time", req.LongQueryTime)
|
||||
config = confval.INI.Set(config, "slow_query_log", req.SlowQueryLog)
|
||||
config = confval.INI.Set(config, "long_query_time", req.LongQueryTime)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -386,76 +356,538 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getINIValue 从 INI 格式内容中获取指定键的值
|
||||
func (s *App) getINIValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, ";") || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "[") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
// ProcessList 获取进程列表
|
||||
func (s *App) ProcessList(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
return ""
|
||||
defer mysql.Close()
|
||||
|
||||
rows, err := mysql.Query(`
|
||||
SELECT ID, coalesce(USER,''), coalesce(HOST,''), coalesce(DB,''), coalesce(COMMAND,''),
|
||||
TIME, coalesce(STATE,''), coalesce(INFO,'')
|
||||
FROM information_schema.PROCESSLIST WHERE ID != CONNECTION_ID() ORDER BY TIME DESC`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
processes := make([]Process, 0)
|
||||
for rows.Next() {
|
||||
var item Process
|
||||
if err = rows.Scan(&item.ID, &item.User, &item.Host, &item.DB, &item.Command, &item.Time, &item.State, &item.Info); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
processes = append(processes, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, processes)
|
||||
}
|
||||
|
||||
// setINIValue 在 INI 格式内容中设置指定键的值
|
||||
func (s *App) setINIValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
// KillProcess 终止进程
|
||||
func (s *App) KillProcess(w http.ResponseWriter, r *http.Request) {
|
||||
id := cast.ToInt64(chi.URLParam(r, "id"))
|
||||
if id <= 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid process id"))
|
||||
return
|
||||
}
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "[") {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, ";") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
} else if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.SplitN(checkLine, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, ";") && !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+line)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+" = "+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" = "+value)
|
||||
defer mysql.Close()
|
||||
|
||||
// KILL 不支持预编译参数,id 已校验为正整数
|
||||
if _, err = mysql.Exec(fmt.Sprintf(`KILL %d`, id)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// TransactionList 获取事务及锁等待列表
|
||||
func (s *App) TransactionList(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
rows, err := mysql.Query(`
|
||||
SELECT trx_id, trx_mysql_thread_id, coalesce(trx_state,''), coalesce(trx_query,''),
|
||||
timestampdiff(SECOND, trx_started, now()), trx_rows_locked, trx_rows_modified
|
||||
FROM information_schema.INNODB_TRX ORDER BY trx_started`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
result := Transactions{Transactions: make([]Transaction, 0), LockWaits: make([]LockWait, 0)}
|
||||
for rows.Next() {
|
||||
var item Transaction
|
||||
if err = rows.Scan(&item.ID, &item.ThreadID, &item.State, &item.Query, &item.Seconds, &item.RowsLocked, &item.RowsModified); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
result.Transactions = append(result.Transactions, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 锁等待对,MariaDB 与 MySQL 8 的表不同,查询失败时忽略
|
||||
lockSQL := `
|
||||
SELECT r.trx_mysql_thread_id, coalesce(r.trx_query,''), b.trx_mysql_thread_id, coalesce(b.trx_query,'')
|
||||
FROM performance_schema.data_lock_waits w
|
||||
JOIN information_schema.innodb_trx r ON r.trx_id = w.REQUESTING_ENGINE_TRANSACTION_ID
|
||||
JOIN information_schema.innodb_trx b ON b.trx_id = w.BLOCKING_ENGINE_TRANSACTION_ID`
|
||||
if s.isMariaDB(mysql) {
|
||||
lockSQL = `
|
||||
SELECT r.trx_mysql_thread_id, coalesce(r.trx_query,''), b.trx_mysql_thread_id, coalesce(b.trx_query,'')
|
||||
FROM information_schema.INNODB_LOCK_WAITS w
|
||||
JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id
|
||||
JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id`
|
||||
}
|
||||
if lockRows, lockErr := mysql.Query(lockSQL); lockErr == nil {
|
||||
defer func() { _ = lockRows.Close() }()
|
||||
for lockRows.Next() {
|
||||
var item LockWait
|
||||
if err = lockRows.Scan(&item.WaitingThreadID, &item.WaitingQuery, &item.BlockingThreadID, &item.BlockingQuery); err != nil {
|
||||
break
|
||||
}
|
||||
result.LockWaits = append(result.LockWaits, item)
|
||||
}
|
||||
}
|
||||
|
||||
service.Success(w, result)
|
||||
}
|
||||
|
||||
// TopSQL 获取 SQL 性能统计
|
||||
func (s *App) TopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
var enabled int
|
||||
if err = mysql.QueryRow(`SELECT @@performance_schema`).Scan(&enabled); err != nil {
|
||||
if strings.Contains(err.Error(), "Unknown system variable") {
|
||||
service.Success(w, TopSQL{Supported: false, Items: []TopSQLItem{}})
|
||||
return
|
||||
}
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if enabled == 0 {
|
||||
// 检查是否已配置等待重启
|
||||
config, _ := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
pending := strings.EqualFold(confval.SectionINI.GetIn(config, "mysqld", "performance_schema"), "on")
|
||||
service.Success(w, TopSQL{Supported: true, Enabled: false, PendingRestart: pending, Items: []TopSQLItem{}})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := mysql.Query(`
|
||||
SELECT coalesce(SCHEMA_NAME,''), COUNT_STAR, SUM_TIMER_WAIT DIV 1000000000,
|
||||
round(AVG_TIMER_WAIT/1e9,2), SUM_ROWS_SENT, SUM_ROWS_EXAMINED, coalesce(DIGEST_TEXT,'')
|
||||
FROM performance_schema.events_statements_summary_by_digest
|
||||
WHERE DIGEST_TEXT IS NOT NULL ORDER BY SUM_TIMER_WAIT DESC LIMIT 50`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
items := make([]TopSQLItem, 0)
|
||||
for rows.Next() {
|
||||
var item TopSQLItem
|
||||
if err = rows.Scan(&item.Database, &item.Calls, &item.TotalMs, &item.MeanMs, &item.RowsSent, &item.RowsExamined, &item.Query); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, TopSQL{Supported: true, Enabled: true, Items: items})
|
||||
}
|
||||
|
||||
// EnableTopSQL 启用 performance_schema
|
||||
func (s *App) EnableTopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
// 实例不支持时禁止写入配置
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
var enabled int
|
||||
if err = mysql.QueryRow(`SELECT @@performance_schema`).Scan(&enabled); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("performance_schema is not supported by this instance"))
|
||||
return
|
||||
}
|
||||
|
||||
confPath := app.Root + "/server/mysql/conf/my.cnf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// my.cnf 分段,必须用 SectionINI 保证写入 [mysqld] 段内,重启后生效
|
||||
config = confval.SectionINI.SetIn(config, "mysqld", "performance_schema", "on")
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ResetTopSQL 重置 SQL 性能统计
|
||||
func (s *App) ResetTopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
if _, err = mysql.Exec(`TRUNCATE TABLE performance_schema.events_statements_summary_by_digest`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// DatabaseList 获取业务数据库列表
|
||||
func (s *App) DatabaseList(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
rows, err := mysql.Query(`
|
||||
SELECT SCHEMA_NAME FROM information_schema.SCHEMATA
|
||||
WHERE SCHEMA_NAME NOT IN ('mysql','information_schema','performance_schema','sys')
|
||||
ORDER BY SCHEMA_NAME`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
databases := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var name string
|
||||
if err = rows.Scan(&name); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
databases = append(databases, name)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, databases)
|
||||
}
|
||||
|
||||
// TableList 获取表维护信息
|
||||
func (s *App) TableList(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[TableQuery](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
query := `
|
||||
SELECT table_schema, table_name, coalesce(engine,''), coalesce(table_rows,0),
|
||||
coalesce(data_length + index_length,0), coalesce(data_free,0)
|
||||
FROM information_schema.TABLES
|
||||
WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys')
|
||||
AND table_type = 'BASE TABLE'`
|
||||
var args []any
|
||||
if req.Database != "" {
|
||||
query += ` AND table_schema = ?`
|
||||
args = append(args, req.Database)
|
||||
}
|
||||
query += ` ORDER BY data_length + index_length DESC LIMIT 50`
|
||||
rows, err := mysql.Query(query, args...)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
items := make([]TableInfo, 0)
|
||||
for rows.Next() {
|
||||
var item TableInfo
|
||||
var size, free int64
|
||||
if err = rows.Scan(&item.Database, &item.Table, &item.Engine, &item.Rows, &size, &free); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
item.Size = tools.FormatBytes(float64(size))
|
||||
item.SizeBytes = size
|
||||
if size+free > 0 {
|
||||
item.FragmentRate = float64(free) * 100 / float64(size+free)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, items)
|
||||
}
|
||||
|
||||
// RunMaintenance 对表执行维护操作(异步任务)
|
||||
func (s *App) RunMaintenance(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[MaintenanceRun](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Tables) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("no tables selected"))
|
||||
return
|
||||
}
|
||||
if !slices.Contains([]string{"optimize", "analyze"}, req.Operation) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid operation"))
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword, err := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tables := make([]string, 0, len(req.Tables))
|
||||
for _, table := range req.Tables {
|
||||
tables = append(tables, fmt.Sprintf("`%s`.`%s`", table.Database, table.Table))
|
||||
}
|
||||
escaped := strings.ReplaceAll(rootPassword, `'`, `'\''`)
|
||||
cmd := fmt.Sprintf("MYSQL_PWD='%s' mysql -u root -e '%s TABLE %s'", escaped, strings.ToUpper(req.Operation), strings.Join(tables, ", "))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = "mysql:maintenance"
|
||||
task.Name = s.t.Get("Run %[1]s on %[2]d tables", req.Operation, len(req.Tables))
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// BinlogList 获取 binlog 状态
|
||||
func (s *App) BinlogList(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
var enabled int
|
||||
if err = mysql.QueryRow(`SELECT @@log_bin`).Scan(&enabled); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if enabled == 0 {
|
||||
service.Success(w, Binlog{Enabled: false, Items: []BinlogFile{}})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := mysql.Query(`SHOW BINARY LOGS`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
// SHOW BINARY LOGS 列数两派不同(MySQL 8 多 Encrypted 列),动态取列
|
||||
maps, err := s.rowsToMaps(rows)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
binlog := Binlog{Enabled: true, Items: make([]BinlogFile, 0, len(maps))}
|
||||
var total int64
|
||||
for _, m := range maps {
|
||||
size := cast.ToInt64(m["File_size"])
|
||||
total += size
|
||||
binlog.Items = append(binlog.Items, BinlogFile{Name: m["Log_name"], Size: tools.FormatBytes(float64(size)), SizeBytes: size})
|
||||
}
|
||||
binlog.TotalSize = tools.FormatBytes(float64(total))
|
||||
|
||||
service.Success(w, binlog)
|
||||
}
|
||||
|
||||
// PurgeBinlog 清理指定文件之前的 binlog
|
||||
func (s *App) PurgeBinlog(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[BinlogPurge](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
// 校验文件名存在于 binlog 列表,PURGE 不支持预编译参数
|
||||
rows, err := mysql.Query(`SHOW BINARY LOGS`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
maps, err := s.rowsToMaps(rows)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if !slices.ContainsFunc(maps, func(m map[string]string) bool { return m["Log_name"] == req.File }) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("binlog file %s does not exist", req.File))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = mysql.Exec(fmt.Sprintf(`PURGE BINARY LOGS TO '%s'`, req.File)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ReplicationStatus 获取复制状态
|
||||
func (s *App) ReplicationStatus(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
rows, err := mysql.Query(`SHOW REPLICA STATUS`)
|
||||
if err != nil {
|
||||
// 老版本 fallback
|
||||
if rows, err = mysql.Query(`SHOW SLAVE STATUS`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
maps, err := s.rowsToMaps(rows)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if len(maps) == 0 {
|
||||
service.Success(w, Replication{Enabled: false})
|
||||
return
|
||||
}
|
||||
|
||||
// MySQL 8 与 MariaDB 的列名两派不同,按候选名取值
|
||||
pick := func(m map[string]string, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if v, ok := m[key]; ok && v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
m := maps[0]
|
||||
service.Success(w, Replication{
|
||||
Enabled: true,
|
||||
IORunning: pick(m, "Replica_IO_Running", "Slave_IO_Running"),
|
||||
SQLRunning: pick(m, "Replica_SQL_Running", "Slave_SQL_Running"),
|
||||
SecondsBehind: pick(m, "Seconds_Behind_Source", "Seconds_Behind_Master"),
|
||||
SourceHost: pick(m, "Source_Host", "Master_Host"),
|
||||
LastError: pick(m, "Last_Error"),
|
||||
})
|
||||
}
|
||||
|
||||
// connect 以 root 用户通过 unix socket 连接
|
||||
func (s *App) connect(ctx context.Context) (db.Operator, error) {
|
||||
rootPassword, err := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.NewMySQL(ctx, "root", rootPassword, db.MySQLSocket(app.Root), "unix")
|
||||
}
|
||||
|
||||
// isMariaDB 判断当前实例是否为 MariaDB
|
||||
func (s *App) isMariaDB(op db.Operator) bool {
|
||||
var version string
|
||||
if err := op.QueryRow(`SELECT VERSION()`).Scan(&version); err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(strings.ToLower(version), "mariadb")
|
||||
}
|
||||
|
||||
// rowsToMaps 将查询结果按列名转为 map,用于列名/列数不固定的查询
|
||||
func (s *App) rowsToMaps(rows *sql.Rows) ([]map[string]string, error) {
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]map[string]string, 0)
|
||||
for rows.Next() {
|
||||
values := make([]sql.NullString, len(columns))
|
||||
scans := make([]any, len(columns))
|
||||
for i := range values {
|
||||
scans[i] = &values[i]
|
||||
}
|
||||
if err = rows.Scan(scans...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]string, len(columns))
|
||||
for i, column := range columns {
|
||||
m[column] = values[i].String
|
||||
}
|
||||
result = append(result, m)
|
||||
}
|
||||
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ type UpdateConfig struct {
|
||||
}
|
||||
|
||||
type SetRootPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
// ConfigTune MySQL 配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
Port string `form:"port" json:"port"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections"`
|
||||
MaxConnectErrors string `form:"max_connect_errors" json:"max_connect_errors"`
|
||||
DefaultStorageEngine string `form:"default_storage_engine" json:"default_storage_engine"`
|
||||
@@ -32,7 +32,7 @@ type ConfigTune struct {
|
||||
// InnoDB
|
||||
InnodbBufferPoolSize string `form:"innodb_buffer_pool_size" json:"innodb_buffer_pool_size"`
|
||||
InnodbLogBufferSize string `form:"innodb_log_buffer_size" json:"innodb_log_buffer_size"`
|
||||
InnodbFlushLogAtTrxCommit string `form:"innodb_flush_log_at_trx_commit" json:"innodb_flush_log_at_trx_commit"`
|
||||
InnodbFlushLogAtTrxCommit string `form:"innodb_flush_log_at_trx_commit" json:"innodb_flush_log_at_trx_commit" validate:"in:0,1,2"`
|
||||
InnodbLockWaitTimeout string `form:"innodb_lock_wait_timeout" json:"innodb_lock_wait_timeout"`
|
||||
InnodbMaxDirtyPagesPct string `form:"innodb_max_dirty_pages_pct" json:"innodb_max_dirty_pages_pct"`
|
||||
InnodbReadIoThreads string `form:"innodb_read_io_threads" json:"innodb_read_io_threads"`
|
||||
@@ -41,3 +41,116 @@ type ConfigTune struct {
|
||||
SlowQueryLog string `form:"slow_query_log" json:"slow_query_log"`
|
||||
LongQueryTime string `form:"long_query_time" json:"long_query_time"`
|
||||
}
|
||||
|
||||
// TableQuery 表维护列表查询请求
|
||||
type TableQuery struct {
|
||||
Database string `form:"database" json:"database"`
|
||||
}
|
||||
|
||||
// MaintenanceTable 维护操作的目标表
|
||||
type MaintenanceTable struct {
|
||||
Database string `form:"database" json:"database" validate:"required"`
|
||||
Table string `form:"table" json:"table" validate:"required"`
|
||||
}
|
||||
|
||||
// MaintenanceRun 表维护操作请求
|
||||
type MaintenanceRun struct {
|
||||
Tables []MaintenanceTable `form:"tables" json:"tables"`
|
||||
Operation string `form:"operation" json:"operation" validate:"required"`
|
||||
}
|
||||
|
||||
// BinlogPurge binlog 清理请求
|
||||
type BinlogPurge struct {
|
||||
File string `form:"file" json:"file" validate:"required"`
|
||||
}
|
||||
|
||||
// Process 数据库进程信息
|
||||
type Process struct {
|
||||
ID int64 `json:"id"`
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
DB string `json:"db"`
|
||||
Command string `json:"command"`
|
||||
Time int64 `json:"time"`
|
||||
State string `json:"state"`
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
||||
// Transaction InnoDB 事务信息
|
||||
type Transaction struct {
|
||||
ID string `json:"id"`
|
||||
ThreadID int64 `json:"thread_id"`
|
||||
State string `json:"state"`
|
||||
Query string `json:"query"`
|
||||
Seconds int64 `json:"seconds"`
|
||||
RowsLocked int64 `json:"rows_locked"`
|
||||
RowsModified int64 `json:"rows_modified"`
|
||||
}
|
||||
|
||||
// LockWait 锁等待对
|
||||
type LockWait struct {
|
||||
WaitingThreadID int64 `json:"waiting_thread_id"`
|
||||
WaitingQuery string `json:"waiting_query"`
|
||||
BlockingThreadID int64 `json:"blocking_thread_id"`
|
||||
BlockingQuery string `json:"blocking_query"`
|
||||
}
|
||||
|
||||
// Transactions 事务与锁等待响应
|
||||
type Transactions struct {
|
||||
Transactions []Transaction `json:"transactions"`
|
||||
LockWaits []LockWait `json:"lock_waits"`
|
||||
}
|
||||
|
||||
// TopSQLItem Top SQL 统计项
|
||||
type TopSQLItem struct {
|
||||
Database string `json:"database"`
|
||||
Calls int64 `json:"calls"`
|
||||
TotalMs int64 `json:"total_ms"`
|
||||
MeanMs float64 `json:"mean_ms"`
|
||||
RowsSent int64 `json:"rows_sent"`
|
||||
RowsExamined int64 `json:"rows_examined"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
// TopSQL Top SQL 响应
|
||||
type TopSQL struct {
|
||||
Supported bool `json:"supported"`
|
||||
Enabled bool `json:"enabled"`
|
||||
PendingRestart bool `json:"pending_restart"`
|
||||
Items []TopSQLItem `json:"items"`
|
||||
}
|
||||
|
||||
// TableInfo 表维护信息
|
||||
type TableInfo struct {
|
||||
Database string `json:"database"`
|
||||
Table string `json:"table"`
|
||||
Engine string `json:"engine"`
|
||||
Rows int64 `json:"rows"`
|
||||
Size string `json:"size"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
FragmentRate float64 `json:"fragment_rate"`
|
||||
}
|
||||
|
||||
// BinlogFile binlog 文件信息
|
||||
type BinlogFile struct {
|
||||
Name string `json:"name"`
|
||||
Size string `json:"size"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
// Binlog binlog 状态响应
|
||||
type Binlog struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
TotalSize string `json:"total_size"`
|
||||
Items []BinlogFile `json:"items"`
|
||||
}
|
||||
|
||||
// Replication 复制状态响应
|
||||
type Replication struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
IORunning string `json:"io_running"`
|
||||
SQLRunning string `json:"sql_running"`
|
||||
SecondsBehind string `json:"seconds_behind"`
|
||||
SourceHost string `json:"source_host"`
|
||||
LastError string `json:"last_error"`
|
||||
}
|
||||
|
||||
+60
-131
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
@@ -26,6 +26,7 @@ type App struct {
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
@@ -50,8 +51,13 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Delete("/stream/upstreams/{name}", s.DeleteStreamUpstream)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("nginx")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/nginx/conf/nginx.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -67,7 +73,7 @@ func (s *App) SaveConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root), req.Config, 0600); err != nil {
|
||||
if err = io.Write(app.Root+"/server/nginx/conf/nginx.conf", req.Config, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -98,7 +104,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get("http://127.0.0.1/nginx_status")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
@@ -172,7 +178,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetConfigTune 获取 Nginx 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/nginx/conf/nginx.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -180,33 +186,33 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tune := ConfigTune{
|
||||
// 常规设置
|
||||
WorkerProcesses: s.getNginxValue(config, "worker_processes"),
|
||||
WorkerConnections: s.getNginxValue(config, "worker_connections"),
|
||||
KeepaliveTimeout: s.getNginxValue(config, "keepalive_timeout"),
|
||||
ClientMaxBodySize: s.getNginxValue(config, "client_max_body_size"),
|
||||
ClientBodyBufferSize: s.getNginxValue(config, "client_body_buffer_size"),
|
||||
ClientHeaderBufferSize: s.getNginxValue(config, "client_header_buffer_size"),
|
||||
ServerNamesHashBucketSize: s.getNginxValue(config, "server_names_hash_bucket_size"),
|
||||
ServerTokens: s.getNginxValue(config, "server_tokens"),
|
||||
WorkerProcesses: confval.Nginx.Get(config, "worker_processes"),
|
||||
WorkerConnections: confval.Nginx.Get(config, "worker_connections"),
|
||||
KeepaliveTimeout: confval.Nginx.Get(config, "keepalive_timeout"),
|
||||
ClientMaxBodySize: confval.Nginx.Get(config, "client_max_body_size"),
|
||||
ClientBodyBufferSize: confval.Nginx.Get(config, "client_body_buffer_size"),
|
||||
ClientHeaderBufferSize: confval.Nginx.Get(config, "client_header_buffer_size"),
|
||||
ServerNamesHashBucketSize: confval.Nginx.Get(config, "server_names_hash_bucket_size"),
|
||||
ServerTokens: confval.Nginx.Get(config, "server_tokens"),
|
||||
// Gzip 压缩
|
||||
Gzip: s.getNginxValue(config, "gzip"),
|
||||
GzipMinLength: s.getNginxValue(config, "gzip_min_length"),
|
||||
GzipCompLevel: s.getNginxValue(config, "gzip_comp_level"),
|
||||
GzipTypes: s.getNginxValue(config, "gzip_types"),
|
||||
GzipVary: s.getNginxValue(config, "gzip_vary"),
|
||||
GzipProxied: s.getNginxValue(config, "gzip_proxied"),
|
||||
Gzip: confval.Nginx.Get(config, "gzip"),
|
||||
GzipMinLength: confval.Nginx.Get(config, "gzip_min_length"),
|
||||
GzipCompLevel: confval.Nginx.Get(config, "gzip_comp_level"),
|
||||
GzipTypes: confval.Nginx.Get(config, "gzip_types"),
|
||||
GzipVary: confval.Nginx.Get(config, "gzip_vary"),
|
||||
GzipProxied: confval.Nginx.Get(config, "gzip_proxied"),
|
||||
// Brotli 压缩
|
||||
Brotli: s.getNginxValue(config, "brotli"),
|
||||
BrotliMinLength: s.getNginxValue(config, "brotli_min_length"),
|
||||
BrotliCompLevel: s.getNginxValue(config, "brotli_comp_level"),
|
||||
BrotliTypes: s.getNginxValue(config, "brotli_types"),
|
||||
BrotliStatic: s.getNginxValue(config, "brotli_static"),
|
||||
Brotli: confval.Nginx.Get(config, "brotli"),
|
||||
BrotliMinLength: confval.Nginx.Get(config, "brotli_min_length"),
|
||||
BrotliCompLevel: confval.Nginx.Get(config, "brotli_comp_level"),
|
||||
BrotliTypes: confval.Nginx.Get(config, "brotli_types"),
|
||||
BrotliStatic: confval.Nginx.Get(config, "brotli_static"),
|
||||
// Zstd 压缩
|
||||
Zstd: s.getNginxValue(config, "zstd"),
|
||||
ZstdMinLength: s.getNginxValue(config, "zstd_min_length"),
|
||||
ZstdCompLevel: s.getNginxValue(config, "zstd_comp_level"),
|
||||
ZstdTypes: s.getNginxValue(config, "zstd_types"),
|
||||
ZstdStatic: s.getNginxValue(config, "zstd_static"),
|
||||
Zstd: confval.Nginx.Get(config, "zstd"),
|
||||
ZstdMinLength: confval.Nginx.Get(config, "zstd_min_length"),
|
||||
ZstdCompLevel: confval.Nginx.Get(config, "zstd_comp_level"),
|
||||
ZstdTypes: confval.Nginx.Get(config, "zstd_types"),
|
||||
ZstdStatic: confval.Nginx.Get(config, "zstd_static"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
@@ -220,7 +226,7 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root)
|
||||
confPath := app.Root + "/server/nginx/conf/nginx.conf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -228,33 +234,33 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 更新常规设置
|
||||
config = s.setNginxValue(config, "worker_processes", req.WorkerProcesses)
|
||||
config = s.setNginxValue(config, "worker_connections", req.WorkerConnections)
|
||||
config = s.setNginxValue(config, "keepalive_timeout", req.KeepaliveTimeout)
|
||||
config = s.setNginxValue(config, "client_max_body_size", req.ClientMaxBodySize)
|
||||
config = s.setNginxValue(config, "client_body_buffer_size", req.ClientBodyBufferSize)
|
||||
config = s.setNginxValue(config, "client_header_buffer_size", req.ClientHeaderBufferSize)
|
||||
config = s.setNginxValue(config, "server_names_hash_bucket_size", req.ServerNamesHashBucketSize)
|
||||
config = s.setNginxValue(config, "server_tokens", req.ServerTokens)
|
||||
config = confval.Nginx.Set(config, "worker_processes", req.WorkerProcesses)
|
||||
config = confval.Nginx.Set(config, "worker_connections", req.WorkerConnections)
|
||||
config = confval.Nginx.Set(config, "keepalive_timeout", req.KeepaliveTimeout)
|
||||
config = confval.Nginx.Set(config, "client_max_body_size", req.ClientMaxBodySize)
|
||||
config = confval.Nginx.Set(config, "client_body_buffer_size", req.ClientBodyBufferSize)
|
||||
config = confval.Nginx.Set(config, "client_header_buffer_size", req.ClientHeaderBufferSize)
|
||||
config = confval.Nginx.Set(config, "server_names_hash_bucket_size", req.ServerNamesHashBucketSize)
|
||||
config = confval.Nginx.Set(config, "server_tokens", req.ServerTokens)
|
||||
// 更新 Gzip 压缩
|
||||
config = s.setNginxValue(config, "gzip", req.Gzip)
|
||||
config = s.setNginxValue(config, "gzip_min_length", req.GzipMinLength)
|
||||
config = s.setNginxValue(config, "gzip_comp_level", req.GzipCompLevel)
|
||||
config = s.setNginxValue(config, "gzip_types", req.GzipTypes)
|
||||
config = s.setNginxValue(config, "gzip_vary", req.GzipVary)
|
||||
config = s.setNginxValue(config, "gzip_proxied", req.GzipProxied)
|
||||
config = confval.Nginx.Set(config, "gzip", req.Gzip)
|
||||
config = confval.Nginx.Set(config, "gzip_min_length", req.GzipMinLength)
|
||||
config = confval.Nginx.Set(config, "gzip_comp_level", req.GzipCompLevel)
|
||||
config = confval.Nginx.Set(config, "gzip_types", req.GzipTypes)
|
||||
config = confval.Nginx.Set(config, "gzip_vary", req.GzipVary)
|
||||
config = confval.Nginx.Set(config, "gzip_proxied", req.GzipProxied)
|
||||
// 更新 Brotli 压缩
|
||||
config = s.setNginxValue(config, "brotli", req.Brotli)
|
||||
config = s.setNginxValue(config, "brotli_min_length", req.BrotliMinLength)
|
||||
config = s.setNginxValue(config, "brotli_comp_level", req.BrotliCompLevel)
|
||||
config = s.setNginxValue(config, "brotli_types", req.BrotliTypes)
|
||||
config = s.setNginxValue(config, "brotli_static", req.BrotliStatic)
|
||||
config = confval.Nginx.Set(config, "brotli", req.Brotli)
|
||||
config = confval.Nginx.Set(config, "brotli_min_length", req.BrotliMinLength)
|
||||
config = confval.Nginx.Set(config, "brotli_comp_level", req.BrotliCompLevel)
|
||||
config = confval.Nginx.Set(config, "brotli_types", req.BrotliTypes)
|
||||
config = confval.Nginx.Set(config, "brotli_static", req.BrotliStatic)
|
||||
// 更新 Zstd 压缩
|
||||
config = s.setNginxValue(config, "zstd", req.Zstd)
|
||||
config = s.setNginxValue(config, "zstd_min_length", req.ZstdMinLength)
|
||||
config = s.setNginxValue(config, "zstd_comp_level", req.ZstdCompLevel)
|
||||
config = s.setNginxValue(config, "zstd_types", req.ZstdTypes)
|
||||
config = s.setNginxValue(config, "zstd_static", req.ZstdStatic)
|
||||
config = confval.Nginx.Set(config, "zstd", req.Zstd)
|
||||
config = confval.Nginx.Set(config, "zstd_min_length", req.ZstdMinLength)
|
||||
config = confval.Nginx.Set(config, "zstd_comp_level", req.ZstdCompLevel)
|
||||
config = confval.Nginx.Set(config, "zstd_types", req.ZstdTypes)
|
||||
config = confval.Nginx.Set(config, "zstd_static", req.ZstdStatic)
|
||||
|
||||
if err = io.Write(confPath, config, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -269,80 +275,3 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getNginxValue 从 Nginx 配置内容中获取指定指令的值
|
||||
func (s *App) getNginxValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(trimmed, ";") {
|
||||
continue
|
||||
}
|
||||
trimmed = strings.TrimSuffix(trimmed, ";")
|
||||
trimmed = strings.TrimSpace(trimmed)
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
return strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setNginxValue 在 Nginx 配置内容中设置指定指令的值
|
||||
func (s *App) setNginxValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || trimmed == "{" || trimmed == "}" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查指令(可能被注释)
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(checkLine, ";") {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine = strings.TrimSuffix(checkLine, ";")
|
||||
checkLine = strings.TrimSpace(checkLine)
|
||||
parts := strings.Fields(checkLine)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
result = append(result, indent+"#"+strings.TrimLeft(line, " \t"))
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 保留原行缩进
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
result = append(result, indent+key+" "+value+";")
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, " "+key+" "+value+";")
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
@@ -39,22 +39,22 @@ type ConfigTune struct {
|
||||
}
|
||||
|
||||
type StreamServer struct {
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 配置名称,用于文件命名
|
||||
Listen string `form:"listen" json:"listen" validate:"required"` // 监听地址,如: "12345", "0.0.0.0:12345", "[::]:12345"
|
||||
UDP bool `form:"udp" json:"udp"` // 是否 UDP 协议
|
||||
ProxyPass string `form:"proxy_pass" json:"proxy_pass" validate:"required"` // 代理地址,如: "127.0.0.1:3306", "upstream_name"
|
||||
ProxyProtocol bool `form:"proxy_protocol" json:"proxy_protocol"` // 是否启用 PROXY 协议
|
||||
ProxyTimeout time.Duration `form:"proxy_timeout" json:"proxy_timeout"` // 代理超时时间
|
||||
ProxyConnectTimeout time.Duration `form:"proxy_connect_timeout" json:"proxy_connect_timeout"` // 代理连接超时时间
|
||||
SSL bool `form:"ssl" json:"ssl"` // 是否启用 SSL
|
||||
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate" validate:"requiredIf:SSL,true"` // SSL 证书路径
|
||||
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key" validate:"requiredIf:SSL,true"` // SSL 私钥路径
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""` // 配置名称,用于文件命名
|
||||
Listen string `form:"listen" json:"listen" validate:"required"` // 监听地址,如: "12345", "0.0.0.0:12345", "[::]:12345"
|
||||
UDP bool `form:"udp" json:"udp"` // 是否 UDP 协议
|
||||
ProxyPass string `form:"proxy_pass" json:"proxy_pass" validate:"required"` // 代理地址,如: "127.0.0.1:3306", "upstream_name"
|
||||
ProxyProtocol bool `form:"proxy_protocol" json:"proxy_protocol"` // 是否启用 PROXY 协议
|
||||
ProxyTimeout time.Duration `form:"proxy_timeout" json:"proxy_timeout"` // 代理超时时间
|
||||
ProxyConnectTimeout time.Duration `form:"proxy_connect_timeout" json:"proxy_connect_timeout"` // 代理连接超时时间
|
||||
SSL bool `form:"ssl" json:"ssl"` // 是否启用 SSL
|
||||
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate" validate:"required_if:SSL,true"` // SSL 证书路径
|
||||
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key" validate:"required_if:SSL,true"` // SSL 私钥路径
|
||||
}
|
||||
|
||||
type StreamUpstream struct {
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 上游名称
|
||||
Servers map[string]string `form:"servers" json:"servers" validate:"required"` // 上游服务器及配置,如: map["127.0.0.1:3306"] = "weight=5"
|
||||
Algo string `form:"algo" json:"algo"` // 负载均衡算法,如: "least_conn", "hash $remote_addr"
|
||||
Resolver []string `form:"resolver" json:"resolver"` // DNS 解析器,如: ["8.8.8.8", "ipv6=off"]
|
||||
ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""` // 上游名称
|
||||
Servers map[string]string `form:"servers" json:"servers" validate:"required"` // 上游服务器及配置,如: map["127.0.0.1:3306"] = "weight=5"
|
||||
Algo string `form:"algo" json:"algo"` // 负载均衡算法,如: "least_conn", "hash $remote_addr"
|
||||
Resolver []string `form:"resolver" json:"resolver"` // DNS 解析器,如: ["8.8.8.8", "ipv6=off"]
|
||||
ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
@@ -35,7 +37,7 @@ func (s *App) CreateStreamServer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", req.Name))
|
||||
configPath := filepath.Join(s.streamDir(), req.Name+".conf")
|
||||
if _, statErr := os.Stat(configPath); statErr == nil {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("stream server config already exists: %s", req.Name))
|
||||
return
|
||||
@@ -69,7 +71,7 @@ func (s *App) UpdateStreamServer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", name))
|
||||
configPath := filepath.Join(s.streamDir(), name+".conf")
|
||||
if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) {
|
||||
service.Error(w, http.StatusNotFound, s.t.Get("stream server not found: %s", name))
|
||||
return
|
||||
@@ -77,7 +79,7 @@ func (s *App) UpdateStreamServer(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
newConfigPath := configPath
|
||||
if req.Name != name {
|
||||
newConfigPath = filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", req.Name))
|
||||
newConfigPath = filepath.Join(s.streamDir(), req.Name+".conf")
|
||||
if _, statErr := os.Stat(newConfigPath); statErr == nil {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("stream server config already exists: %s", req.Name))
|
||||
return
|
||||
@@ -109,7 +111,7 @@ func (s *App) DeleteStreamServer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", name))
|
||||
configPath := filepath.Join(s.streamDir(), name+".conf")
|
||||
if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) {
|
||||
service.Error(w, http.StatusNotFound, s.t.Get("stream server not found: %s", name))
|
||||
return
|
||||
@@ -410,24 +412,24 @@ func (s *App) parseStreamUpstreamFile(filePath string, expectedName string) (*St
|
||||
|
||||
cfg := p.Config()
|
||||
if cfg == nil || cfg.Block == nil {
|
||||
return nil, fmt.Errorf("invalid config")
|
||||
return nil, errors.New("invalid config")
|
||||
}
|
||||
|
||||
// 查找 upstream 块
|
||||
upstreamDirectives := cfg.Block.FindDirectives("upstream")
|
||||
if len(upstreamDirectives) == 0 {
|
||||
return nil, fmt.Errorf("no upstream block found")
|
||||
return nil, errors.New("no upstream block found")
|
||||
}
|
||||
|
||||
upstreamDir := upstreamDirectives[0]
|
||||
params := upstreamDir.GetParameters()
|
||||
if len(params) == 0 {
|
||||
return nil, fmt.Errorf("upstream name not found")
|
||||
return nil, errors.New("upstream name not found")
|
||||
}
|
||||
|
||||
name := params[0].Value
|
||||
if expectedName != "" && name != expectedName {
|
||||
return nil, fmt.Errorf("upstream name mismatch")
|
||||
return nil, errors.New("upstream name mismatch")
|
||||
}
|
||||
|
||||
upstream := &StreamUpstream{
|
||||
@@ -438,7 +440,7 @@ func (s *App) parseStreamUpstreamFile(filePath string, expectedName string) (*St
|
||||
|
||||
upstreamBlock := upstreamDir.GetBlock()
|
||||
if upstreamBlock == nil {
|
||||
return nil, fmt.Errorf("upstream block is empty")
|
||||
return nil, errors.New("upstream block is empty")
|
||||
}
|
||||
|
||||
// 解析 upstream 块中的指令
|
||||
@@ -563,10 +565,7 @@ func (s *App) saveStreamUpstreamConfig(filePath string, upstream *StreamUpstream
|
||||
}
|
||||
|
||||
// 服务器列表
|
||||
var addrs []string
|
||||
for addr := range upstream.Servers {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
addrs := lo.Keys(upstream.Servers)
|
||||
sort.Strings(addrs)
|
||||
|
||||
for _, addr := range addrs {
|
||||
|
||||
@@ -2,21 +2,27 @@ package openresty
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
nginx *nginx.App
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(nginxApp *nginx.App) *App {
|
||||
return &App{
|
||||
nginx: nginx.NewApp(t),
|
||||
nginx: nginxApp,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
s.nginx.Route(r)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("nginx")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -15,6 +14,8 @@ import (
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -26,6 +27,7 @@ type App struct {
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
@@ -37,6 +39,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("opensearch")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("opensearch")
|
||||
if err != nil {
|
||||
@@ -52,7 +59,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/_cluster/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
@@ -87,28 +94,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("opensearch"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "opensearch")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 OpenSearch 配置调整参数
|
||||
@@ -124,13 +114,13 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
heapInit, heapMax := s.parseJVMHeap(jvmRaw)
|
||||
|
||||
tune := ConfigTune{
|
||||
ClusterName: s.getYAMLValue(cfg, "cluster.name"),
|
||||
NodeName: s.getYAMLValue(cfg, "node.name"),
|
||||
NetworkHost: s.getYAMLValue(cfg, "network.host"),
|
||||
HTTPPort: s.getYAMLValue(cfg, "http.port"),
|
||||
DiscoveryType: s.getYAMLValue(cfg, "discovery.type"),
|
||||
PathData: s.getYAMLValue(cfg, "path.data"),
|
||||
PathLogs: s.getYAMLValue(cfg, "path.logs"),
|
||||
ClusterName: confval.GetYAML(cfg, "cluster.name"),
|
||||
NodeName: confval.GetYAML(cfg, "node.name"),
|
||||
NetworkHost: confval.GetYAML(cfg, "network.host"),
|
||||
HTTPPort: confval.GetYAML(cfg, "http.port"),
|
||||
DiscoveryType: confval.GetYAML(cfg, "discovery.type"),
|
||||
PathData: confval.GetYAML(cfg, "path.data"),
|
||||
PathLogs: confval.GetYAML(cfg, "path.logs"),
|
||||
HeapInitSize: heapInit,
|
||||
HeapMaxSize: heapMax,
|
||||
}
|
||||
@@ -152,13 +142,13 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
s.setYAMLValue(cfg, "cluster.name", req.ClusterName)
|
||||
s.setYAMLValue(cfg, "node.name", req.NodeName)
|
||||
s.setYAMLValue(cfg, "network.host", req.NetworkHost)
|
||||
s.setYAMLValue(cfg, "http.port", req.HTTPPort)
|
||||
s.setYAMLValue(cfg, "discovery.type", req.DiscoveryType)
|
||||
s.setYAMLValue(cfg, "path.data", req.PathData)
|
||||
s.setYAMLValue(cfg, "path.logs", req.PathLogs)
|
||||
confval.SetYAML(cfg, "cluster.name", req.ClusterName)
|
||||
confval.SetYAML(cfg, "node.name", req.NodeName)
|
||||
confval.SetYAML(cfg, "network.host", req.NetworkHost)
|
||||
confval.SetYAML(cfg, "http.port", req.HTTPPort)
|
||||
confval.SetYAML(cfg, "discovery.type", req.DiscoveryType)
|
||||
confval.SetYAML(cfg, "path.data", req.PathData)
|
||||
confval.SetYAML(cfg, "path.logs", req.PathLogs)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
@@ -188,11 +178,11 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/opensearch/config/opensearch.yml", app.Root)
|
||||
return app.Root + "/server/opensearch/config/opensearch.yml"
|
||||
}
|
||||
|
||||
func (s *App) jvmOptionsPath() string {
|
||||
return fmt.Sprintf("%s/server/opensearch/config/jvm.options", app.Root)
|
||||
return app.Root + "/server/opensearch/config/jvm.options"
|
||||
}
|
||||
|
||||
func (s *App) getPort() string {
|
||||
@@ -200,53 +190,13 @@ func (s *App) getPort() string {
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := s.getYAMLValue(cfg, "http.port"); v != "" {
|
||||
if v := confval.GetYAML(cfg, "http.port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "9200"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取 YAML 值,优先匹配平铺键(如 "path.data"),回退到嵌套键(如 path -> data)
|
||||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||||
// 优先匹配平铺键(安装脚本用 sed 生成的格式)
|
||||
if val, ok := cfg[key]; ok {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
// 回退到嵌套键
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
val, ok := cfg[parts[0]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
nested, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s.getYAMLValue(nested, parts[1])
|
||||
}
|
||||
|
||||
// setYAMLValue 设置 YAML 值
|
||||
func (s *App) setYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
// 使用平铺键,同时清理可能存在的嵌套键
|
||||
cfg[key] = value
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
if len(parts) == 2 {
|
||||
if nested, ok := cfg[parts[0]].(map[string]any); ok {
|
||||
delete(nested, parts[1])
|
||||
if len(nested) == 0 {
|
||||
delete(cfg, parts[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) parseJVMHeap(content string) (initSize, maxSize string) {
|
||||
reInit := regexp.MustCompile(`(?m)^-Xms(\S+)`)
|
||||
reMax := regexp.MustCompile(`(?m)^-Xmx(\S+)`)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package opensearch
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune OpenSearch 配置调整
|
||||
type ConfigTune struct {
|
||||
// 集群
|
||||
|
||||
@@ -2,22 +2,24 @@ package percona
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
mysql *mysql.App
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(mysqlApp *mysql.App) *App {
|
||||
return &App{
|
||||
mysql: mysql.NewApp(t, setting, databaseServer),
|
||||
mysql: mysqlApp,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
s.mysql.Route(r)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
return s.mysql.Status()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,548 @@
|
||||
package pgadmin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
stdio "io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/firewall"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
conf *config.Config
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
conf: conf,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// pgadminLanguage 面板语言映射为 pgAdmin 语言代码
|
||||
func (s *App) pgadminLanguage() string {
|
||||
switch s.conf.App.Locale {
|
||||
case "":
|
||||
return ""
|
||||
case "zh_CN":
|
||||
return "zh_Hans_CN"
|
||||
case "zh_TW":
|
||||
return "zh_Hant_TW"
|
||||
default:
|
||||
return s.conf.App.Locale
|
||||
}
|
||||
}
|
||||
|
||||
// clientIP 获取请求来源 IP,面板位于反代之后时优先取配置的 IP 头
|
||||
func (s *App) clientIP(r *http.Request) string {
|
||||
ip := r.RemoteAddr
|
||||
if header := s.conf.HTTP.IPHeader; header != "" && r.Header.Get(header) != "" {
|
||||
ip = strings.TrimSpace(strings.Split(r.Header.Get(header), ",")[0])
|
||||
}
|
||||
if host, _, err := net.SplitHostPort(ip); err == nil {
|
||||
ip = host
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/info", s.Info)
|
||||
r.Post("/port", s.UpdatePort)
|
||||
r.Post("/login", s.Login)
|
||||
r.Post("/update_username", s.UpdateUsername)
|
||||
r.Post("/reset_password", s.ResetPassword)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("pgadmin")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) path() string {
|
||||
return app.Root + "/server/pgadmin"
|
||||
}
|
||||
|
||||
// port 从 systemd 环境文件中解析监听端口
|
||||
func (s *App) port() (uint, error) {
|
||||
conf, err := io.Read(s.path() + "/pgadmin.conf")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
match := regexp.MustCompile(`PGADMIN_LISTEN=.+:(\d+)`).FindStringSubmatch(conf)
|
||||
if len(match) < 2 {
|
||||
return 0, errors.New(s.t.Get("pgAdmin port not found"))
|
||||
}
|
||||
|
||||
return cast.ToUint(match[1]), nil
|
||||
}
|
||||
|
||||
// credential 读取安装时生成的初始凭据(邮箱与密码)
|
||||
func (s *App) credential() (string, string) {
|
||||
raw, err := io.Read(s.path() + "/credential")
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(raw), "\n")
|
||||
if len(lines) < 2 {
|
||||
return strings.TrimSpace(lines[0]), ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(lines[0]), strings.TrimSpace(lines[1])
|
||||
}
|
||||
|
||||
func (s *App) Info(w http.ResponseWriter, r *http.Request) {
|
||||
port, err := s.port()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
email, password := s.credential()
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"port": port,
|
||||
"email": email,
|
||||
"password": password,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdatePort](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
conf := s.path() + "/pgadmin.conf"
|
||||
content, err := io.Read(conf)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
content = regexp.MustCompile(`PGADMIN_LISTEN=(.+):\d+`).ReplaceAllString(content, "PGADMIN_LISTEN=${1}:"+cast.ToString(req.Port))
|
||||
if err = io.Write(conf, content, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fw := firewall.NewFirewall()
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
Direction: firewall.DirectionIn,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("pgadmin"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to restart pgAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// serversFile pgAdmin dump-servers/load-servers 的 JSON 结构
|
||||
type serversFile struct {
|
||||
Servers map[string]serverEntry `json:"Servers"`
|
||||
}
|
||||
|
||||
type serverEntry struct {
|
||||
Name string `json:"Name"`
|
||||
Group string `json:"Group"`
|
||||
Host string `json:"Host"`
|
||||
Port int `json:"Port"`
|
||||
MaintenanceDB string `json:"MaintenanceDB"`
|
||||
Username string `json:"Username"`
|
||||
SSLMode string `json:"SSLMode,omitempty"`
|
||||
PassFile string `json:"PassFile,omitempty"`
|
||||
}
|
||||
|
||||
// escapePgpass 转义 pgpass 字段中的反斜杠与冒号
|
||||
func escapePgpass(s string) string {
|
||||
return strings.NewReplacer(`\`, `\\`, `:`, `\:`).Replace(s)
|
||||
}
|
||||
|
||||
// existingServers 只读 pgAdmin 配置库查询 Servers 组内已注册的服务器
|
||||
// CLI 每次调用都要冷启动整个 pgAdmin 应用,直读库快数个量级
|
||||
func (s *App) existingServers(email string) (map[string]struct{}, error) {
|
||||
db, err := sql.Open("sqlite", fmt.Sprintf("file:%s/data/pgadmin.db?mode=ro", s.path()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
rows, err := db.Query(`SELECT s.host, s.port, s.username FROM server s
|
||||
JOIN servergroup g ON s.servergroup_id = g.id
|
||||
JOIN "user" u ON s.user_id = u.id
|
||||
WHERE u.email = ? AND g.name = 'Servers'`, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
existing := make(map[string]struct{})
|
||||
for rows.Next() {
|
||||
var host, username sql.NullString
|
||||
var port sql.NullInt64
|
||||
if err = rows.Scan(&host, &port, &username); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
existing[fmt.Sprintf("%s:%d:%s", host.String, port.Int64, username.String)] = struct{}{}
|
||||
}
|
||||
return existing, rows.Err()
|
||||
}
|
||||
|
||||
// dumpExistingServers 通过 CLI 导出查询已注册服务器,直读配置库失败时的回退路径
|
||||
func (s *App) dumpExistingServers(email string) map[string]struct{} {
|
||||
dump := filepath.Join(os.TempDir(), "pgadmin-servers.json")
|
||||
defer func() { _ = io.Remove(dump) }()
|
||||
_, _ = shell.Execf("%s/cli dump-servers '%s' --user '%s'", s.path(), dump, email)
|
||||
|
||||
existing := make(map[string]struct{})
|
||||
if raw, err := io.Read(dump); err == nil {
|
||||
var dumped serversFile
|
||||
if err = json.Unmarshal([]byte(raw), &dumped); err == nil {
|
||||
for _, item := range dumped.Servers {
|
||||
// 只认默认 Servers 组内的条目,历史版本同步进其他分组的会在此组补齐
|
||||
if item.Group != "Servers" {
|
||||
continue
|
||||
}
|
||||
existing[fmt.Sprintf("%s:%d:%s", item.Host, item.Port, item.Username)] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
// syncServers 将面板中全部 PostgreSQL 服务器合并注册到 pgAdmin,凭据写入 pgpass 实现免密
|
||||
// 仅追加 pgAdmin 中缺失的服务器,不影响用户在 pgAdmin 中手动添加的内容
|
||||
func (s *App) syncServers(ctx context.Context, email string) error {
|
||||
servers, _, err := s.databaseServerRepo.List(ctx, 1, 10000, string(biz.DatabaseTypePostgresql))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(servers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// pgAdmin server 模式下 PassFile 以用户 storage 目录为根,目录名为邮箱 @ 转 _
|
||||
storageDir := fmt.Sprintf("%s/data/storage/%s", s.path(), strings.ReplaceAll(email, "@", "_"))
|
||||
pgpass := filepath.Join(storageDir, "pgpass")
|
||||
|
||||
// 重写 pgpass 中面板服务器的凭据行,保留其他行
|
||||
prefixes := make([]string, 0, len(servers))
|
||||
entries := make([]string, 0, len(servers))
|
||||
for _, server := range servers {
|
||||
prefix := fmt.Sprintf("%s:%d:*:%s:", escapePgpass(server.Host), server.Port, escapePgpass(server.Username))
|
||||
prefixes = append(prefixes, prefix)
|
||||
entries = append(entries, prefix+escapePgpass(server.Password))
|
||||
}
|
||||
var lines []string
|
||||
if raw, err := io.Read(pgpass); err == nil {
|
||||
for line := range strings.SplitSeq(strings.TrimSpace(raw), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
panelOwned := false
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
panelOwned = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !panelOwned {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
lines = append(lines, entries...)
|
||||
if err = os.MkdirAll(storageDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(pgpass, strings.Join(lines, "\n")+"\n", 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 查询 pgAdmin 已有服务器用于查缺,直读配置库,异常时回退 CLI 导出
|
||||
existing, err := s.existingServers(email)
|
||||
if err != nil {
|
||||
existing = s.dumpExistingServers(email)
|
||||
}
|
||||
|
||||
// 一次性合并导入缺失的服务器
|
||||
missing := make(map[string]serverEntry)
|
||||
for i, server := range servers {
|
||||
if _, ok := existing[fmt.Sprintf("%s:%d:%s", server.Host, server.Port, server.Username)]; ok {
|
||||
continue
|
||||
}
|
||||
missing[cast.ToString(i+1)] = serverEntry{
|
||||
Name: server.Name,
|
||||
Group: "Servers",
|
||||
Host: server.Host,
|
||||
Port: int(server.Port),
|
||||
MaintenanceDB: "postgres",
|
||||
Username: server.Username,
|
||||
SSLMode: "prefer",
|
||||
PassFile: "/pgpass",
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
load := filepath.Join(os.TempDir(), "pgadmin-servers-add.json")
|
||||
defer func() { _ = io.Remove(load) }()
|
||||
payload, err := json.Marshal(serversFile{Servers: missing})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(load, string(payload), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if out, err := shell.Execf("%s/cli load-servers '%s' --user '%s'", s.path(), load, email); err != nil {
|
||||
return errors.Join(err, errors.New(out))
|
||||
}
|
||||
// CLI 以 root 运行,修正数据目录属主避免服务写入失败
|
||||
if _, err = shell.Execf("chown -R www:www %s/data", s.path()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 常态路径仅写入了 pgpass,精确修正属主即可
|
||||
if err = io.Chown(storageDir, "www", "www"); err != nil {
|
||||
return err
|
||||
}
|
||||
return io.Chown(pgpass, "www", "www")
|
||||
}
|
||||
|
||||
// Login 同步面板全部 PostgreSQL 服务器后代理登录 pgAdmin 并将会话 Cookie 转发给浏览器
|
||||
// 面板与 pgAdmin 同主机不同端口,Cookie 按主机共享,浏览器凭转发的 Cookie 即为已登录态
|
||||
func (s *App) Login(w http.ResponseWriter, r *http.Request) {
|
||||
port, err := s.port()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
email, password := s.credential()
|
||||
if email == "" || password == "" {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("pgAdmin credential file not found"))
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.syncServers(r.Context(), email); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to sync servers to pgAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
// 透传浏览器 IP 与 UA,pgAdmin 增强 Cookie 保护将会话绑定到 sha256(IP|UA),
|
||||
// 伪装成浏览器身份登录后浏览器直连即可通过校验,无需关闭该保护
|
||||
clientIP := s.clientIP(r)
|
||||
clientUA := r.UserAgent()
|
||||
|
||||
// 获取登录页以取得会话 Cookie 与 CSRF token
|
||||
loginURL := fmt.Sprintf("http://127.0.0.1:%d/login", port)
|
||||
pageReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, loginURL, nil)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
pageReq.Header.Set("User-Agent", clientUA)
|
||||
pageReq.Header.Set("X-Forwarded-For", clientIP)
|
||||
pageResp, err := client.Do(pageReq)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request pgAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = pageResp.Body.Close() }()
|
||||
page, err := stdio.ReadAll(stdio.LimitReader(pageResp.Body, 4<<20))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request pgAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 登录页为 React 渲染,CSRF token 在内嵌 JSON 中,保留 input 形态兼容旧版本
|
||||
csrf := regexp.MustCompile(`"csrfToken":\s*"([^"]+)"`).FindStringSubmatch(string(page))
|
||||
if len(csrf) < 2 {
|
||||
csrf = regexp.MustCompile(`name="csrf_token"[^>]*value="([^"]+)"`).FindStringSubmatch(string(page))
|
||||
}
|
||||
if len(csrf) < 2 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to parse pgAdmin login page"))
|
||||
return
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("csrf_token", csrf[1])
|
||||
form.Set("email", email)
|
||||
form.Set("password", password)
|
||||
// 语言跟随面板设置,pgAdmin 会将 language 字段固化进会话
|
||||
if lang := s.pgadminLanguage(); lang != "" {
|
||||
form.Set("language", lang)
|
||||
}
|
||||
|
||||
loginReq, err := http.NewRequestWithContext(r.Context(), http.MethodPost, fmt.Sprintf("http://127.0.0.1:%d/authenticate/login", port), strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
loginReq.Header.Set("User-Agent", clientUA)
|
||||
loginReq.Header.Set("X-Forwarded-For", clientIP)
|
||||
for _, cookie := range pageResp.Cookies() {
|
||||
loginReq.AddCookie(cookie)
|
||||
}
|
||||
|
||||
loginResp, err := client.Do(loginReq)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request pgAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = loginResp.Body.Close() }()
|
||||
_, _ = stdio.Copy(stdio.Discard, stdio.LimitReader(loginResp.Body, 4<<20))
|
||||
|
||||
// 登录成功时 pgAdmin 返回 302 且跳转目标不是登录页
|
||||
location := loginResp.Header.Get("Location")
|
||||
if loginResp.StatusCode != http.StatusFound || strings.Contains(location, "/login") {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to login pgAdmin, please check the credential of pgAdmin"))
|
||||
return
|
||||
}
|
||||
|
||||
// 改写 SameSite 以兼容面板 https 跳转 http 的场景
|
||||
for _, cookie := range append(pageResp.Cookies(), loginResp.Cookies()...) {
|
||||
cookie.SameSite = http.SameSiteLaxMode
|
||||
cookie.Secure = false
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"port": port,
|
||||
})
|
||||
}
|
||||
|
||||
// ResetPassword 通过 CLI 重置管理员密码并同步凭据文件
|
||||
// UpdateUsername 修改管理员账号,迁移服务器配置与用户存储目录
|
||||
func (s *App) UpdateUsername(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateUsername](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
oldEmail, password := s.credential()
|
||||
if oldEmail == "" || password == "" {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("pgAdmin credential file not found"))
|
||||
return
|
||||
}
|
||||
if req.Username == oldEmail {
|
||||
service.Success(w, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 以当前密码创建新管理员账号
|
||||
if out, err := shell.Execf("%s/cli add-user '%s' '%s' --admin", s.path(), req.Username, password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to create new account: %v", errors.Join(err, errors.New(out))))
|
||||
return
|
||||
}
|
||||
|
||||
// 迁移服务器连接配置到新账号
|
||||
dump := filepath.Join(os.TempDir(), "pgadmin-servers-migrate.json")
|
||||
defer func() { _ = io.Remove(dump) }()
|
||||
_, _ = shell.Execf("%s/cli dump-servers '%s' --user '%s'", s.path(), dump, oldEmail)
|
||||
if io.Exists(dump) {
|
||||
_, _ = shell.Execf("%s/cli load-servers '%s' --user '%s'", s.path(), dump, req.Username)
|
||||
}
|
||||
|
||||
// 删除旧账号
|
||||
if out, err := shell.Execf("%s/cli delete-user '%s' --yes", s.path(), oldEmail); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to delete old account: %v", errors.Join(err, errors.New(out))))
|
||||
return
|
||||
}
|
||||
|
||||
// 迁移用户存储目录(pgpass 等),目录名为邮箱 @ 转 _
|
||||
oldDir := fmt.Sprintf("%s/data/storage/%s", s.path(), strings.ReplaceAll(oldEmail, "@", "_"))
|
||||
newDir := fmt.Sprintf("%s/data/storage/%s", s.path(), strings.ReplaceAll(req.Username, "@", "_"))
|
||||
if io.Exists(oldDir) && !io.Exists(newDir) {
|
||||
_ = os.Rename(oldDir, newDir)
|
||||
}
|
||||
|
||||
// CLI 以 root 运行,修正数据目录属主避免服务写入失败
|
||||
if _, err = shell.Execf("chown -R www:www %s/data", s.path()); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新凭据文件,下次登录按新账号重新同步
|
||||
if err = io.Write(s.path()+"/credential", req.Username+"\n"+password+"\n", 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ResetPassword](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
email, _ := s.credential()
|
||||
if email == "" {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("pgAdmin credential file not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// cli 为安装脚本生成的稳定入口,屏蔽上游命令名随大版本变化
|
||||
if out, err := shell.Execf("%s/cli update-user '%s' --password '%s'", s.path(), email, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reset password: %v", errors.Join(err, errors.New(out))))
|
||||
return
|
||||
}
|
||||
// CLI 以 root 运行,修正数据目录属主避免服务写入失败
|
||||
if _, err = shell.Execf("chown -R www:www %s/data", s.path()); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(s.path()+"/credential", email+"\n"+req.Password+"\n", 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package pgadmin
|
||||
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port" validate:"required && number && min:1 && max:65535"`
|
||||
}
|
||||
|
||||
type ResetPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
type UpdateUsername struct {
|
||||
Username string `form:"username" json:"username" validate:"required && email"`
|
||||
}
|
||||
+189
-20
@@ -1,47 +1,67 @@
|
||||
package phpmyadmin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
stdio "io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/libtnb/utils/str"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/firewall"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
t *gotext.Locale
|
||||
conf *config.Config
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
t: t,
|
||||
conf: conf,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/info", s.Info)
|
||||
r.Post("/port", s.UpdatePort)
|
||||
r.Post("/login", s.Login)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Info(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := os.ReadDir(fmt.Sprintf("%s/server/phpmyadmin", app.Root))
|
||||
// Status phpMyAdmin 由 nginx 站点承载,运行状态与 nginx 一致
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("nginx")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// info 获取 phpMyAdmin 的访问目录与端口
|
||||
func (s *App) info() (string, int, error) {
|
||||
files, err := os.ReadDir(app.Root + "/server/phpmyadmin")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("phpMyAdmin directory not found"))
|
||||
return
|
||||
return "", 0, errors.New(s.t.Get("phpMyAdmin directory not found"))
|
||||
}
|
||||
|
||||
var phpmyadmin string
|
||||
@@ -51,24 +71,173 @@ func (s *App) Info(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
if len(phpmyadmin) == 0 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("phpMyAdmin directory not found"))
|
||||
return
|
||||
return "", 0, errors.New(s.t.Get("phpMyAdmin directory not found"))
|
||||
}
|
||||
|
||||
conf, err := io.Read(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root))
|
||||
conf, err := io.Read(app.Root + "/sites/phpmyadmin/config/nginx.conf")
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf)
|
||||
if len(match) == 0 {
|
||||
return "", 0, errors.New(s.t.Get("phpMyAdmin port not found"))
|
||||
}
|
||||
|
||||
return phpmyadmin, cast.ToInt(match[1]), nil
|
||||
}
|
||||
|
||||
func (s *App) Info(w http.ResponseWriter, r *http.Request) {
|
||||
path, port, err := s.info()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf)
|
||||
if len(match) == 0 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("phpMyAdmin port not found"))
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"path": path,
|
||||
"port": port,
|
||||
})
|
||||
}
|
||||
|
||||
// ensureConfig 为存量安装补写 config.inc.php,允许登录到任意 MySQL 服务器
|
||||
func (s *App) ensureConfig(path string) error {
|
||||
config := fmt.Sprintf("%s/server/phpmyadmin/%s/config.inc.php", app.Root, path)
|
||||
if io.Exists(config) {
|
||||
return nil
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(`<?php
|
||||
declare(strict_types=1);
|
||||
$cfg['blowfish_secret'] = '%s';
|
||||
$cfg['AllowArbitraryServer'] = true;
|
||||
`, str.Random(32))
|
||||
return io.Write(config, content, 0644)
|
||||
}
|
||||
|
||||
// Login 代理登录 phpMyAdmin 并将会话 Cookie 转发给浏览器
|
||||
// 面板与 phpMyAdmin 同主机不同端口,Cookie 按主机共享,浏览器凭转发的 Cookie 即为已登录态
|
||||
func (s *App) Login(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Login](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
server, err := s.databaseServerRepo.Get(r.Context(), req.ServerID)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if server.Type != biz.DatabaseTypeMysql {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("server %s is not a MySQL server", server.Name))
|
||||
return
|
||||
}
|
||||
|
||||
path, port, err := s.info()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.ensureConfig(path); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
loginURL := fmt.Sprintf("http://127.0.0.1:%d/%s/index.php?route=/", port, path)
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
// 获取登录页以取得会话 Cookie 与 CSRF token
|
||||
// 转发浏览器语言,避免语言协商落空后回落英文
|
||||
pageReq, err := http.NewRequestWithContext(r.Context(), http.MethodGet, loginURL, nil)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
pageReq.Header.Set("Accept-Language", r.Header.Get("Accept-Language"))
|
||||
pageResp, err := client.Do(pageReq)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request phpMyAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = pageResp.Body.Close() }()
|
||||
page, err := stdio.ReadAll(stdio.LimitReader(pageResp.Body, 4<<20))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request phpMyAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
token := regexp.MustCompile(`name="token" value="([^"]+)"`).FindStringSubmatch(string(page))
|
||||
session := regexp.MustCompile(`name="set_session" value="([^"]+)"`).FindStringSubmatch(string(page))
|
||||
if len(token) < 2 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to parse phpMyAdmin login page"))
|
||||
return
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("route", "/")
|
||||
// 语言跟随面板设置,登录后固化进 phpMyAdmin 会话
|
||||
if s.conf.App.Locale != "" {
|
||||
form.Set("lang", s.conf.App.Locale)
|
||||
}
|
||||
form.Set("token", html.UnescapeString(token[1]))
|
||||
if len(session) >= 2 {
|
||||
form.Set("set_session", html.UnescapeString(session[1]))
|
||||
}
|
||||
form.Set("pma_username", server.Username)
|
||||
form.Set("pma_password", server.Password)
|
||||
form.Set("server", "1")
|
||||
// 本地默认端口走 phpMyAdmin 默认配置(socket 连接),其余场景显式指定目标服务器
|
||||
// host 为 localhost 但端口非默认时须用 127.0.0.1 强制走 TCP,否则 mysqli 会忽略端口走 socket
|
||||
isLocal := server.Host == "localhost" || server.Host == "127.0.0.1"
|
||||
if !isLocal || server.Port != 3306 {
|
||||
host := server.Host
|
||||
if host == "localhost" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
form.Set("pma_servername", fmt.Sprintf("%s %d", host, server.Port))
|
||||
}
|
||||
|
||||
loginReq, err := http.NewRequestWithContext(r.Context(), http.MethodPost, loginURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
loginReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
loginReq.Header.Set("Accept-Language", r.Header.Get("Accept-Language"))
|
||||
for _, cookie := range pageResp.Cookies() {
|
||||
loginReq.AddCookie(cookie)
|
||||
}
|
||||
|
||||
loginResp, err := client.Do(loginReq)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to request phpMyAdmin: %v", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = loginResp.Body.Close() }()
|
||||
_, _ = stdio.Copy(stdio.Discard, stdio.LimitReader(loginResp.Body, 4<<20))
|
||||
|
||||
// 登录成功时 phpMyAdmin 返回 302 并携带会话 Cookie
|
||||
if loginResp.StatusCode != http.StatusFound {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to login phpMyAdmin, please check the credentials and status of server %s", server.Name))
|
||||
return
|
||||
}
|
||||
|
||||
// 改写 SameSite 以兼容面板 https 跳转 http 的场景
|
||||
for _, cookie := range append(pageResp.Cookies(), loginResp.Cookies()...) {
|
||||
cookie.SameSite = http.SameSiteLaxMode
|
||||
cookie.Secure = false
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"path": phpmyadmin,
|
||||
"port": cast.ToInt(match[1]),
|
||||
"path": path,
|
||||
"port": port,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,13 +248,13 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
conf, err := io.Read(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root))
|
||||
conf, err := io.Read(app.Root + "/sites/phpmyadmin/config/nginx.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+cast.ToString(req.Port)+";")
|
||||
if err = io.Write(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root), conf, 0600); err != nil {
|
||||
if err = io.Write(app.Root+"/sites/phpmyadmin/config/nginx.conf", conf, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -113,7 +282,7 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/sites/phpmyadmin/config/nginx.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -129,7 +298,7 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root), req.Config, 0600); err != nil {
|
||||
if err = io.Write(app.Root+"/sites/phpmyadmin/config/nginx.conf", req.Config, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -5,5 +5,9 @@ type UpdateConfig struct {
|
||||
}
|
||||
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port" validate:"required|number|min:1|max:65535"`
|
||||
Port uint `form:"port" json:"port" validate:"required && number && min:1 && max:65535"`
|
||||
}
|
||||
|
||||
type Login struct {
|
||||
ServerID uint `form:"server_id" json:"server_id" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
@@ -23,6 +24,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/storage_config", s.UpdateStorageConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("podman")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) GetRegistryConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/containers/registries.conf")
|
||||
if err != nil {
|
||||
|
||||
+652
-148
@@ -1,18 +1,25 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
@@ -20,17 +27,23 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
var defaultVersionRegexp = regexp.MustCompile(`default_version\s*=\s*'([^']+)'`)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
conf *config.Config
|
||||
settingRepo biz.SettingRepo
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, setting biz.SettingRepo, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, conf *config.Config, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo, taskRepo biz.TaskRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
conf: conf,
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
taskRepo: taskRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,17 +54,38 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/user_config", s.UpdateUserConfig)
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/log", s.Log)
|
||||
r.Post("/clear_log", s.ClearLog)
|
||||
r.Get("/postgres_password", s.GetPostgresPassword)
|
||||
r.Post("/postgres_password", s.SetPostgresPassword)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
// 扩展管理
|
||||
r.Get("/extensions", s.ExtensionList)
|
||||
r.Post("/extensions", s.InstallExtension)
|
||||
r.Delete("/extensions", s.UninstallExtension)
|
||||
r.Post("/extensions/enable", s.EnableExtension)
|
||||
// 性能
|
||||
r.Get("/sessions", s.SessionList)
|
||||
r.Post("/sessions/{pid}/terminate", s.TerminateSession)
|
||||
r.Get("/top_sql", s.TopSQL)
|
||||
r.Post("/top_sql/enable", s.EnableTopSQL)
|
||||
r.Post("/top_sql/reset", s.ResetTopSQL)
|
||||
// 维护
|
||||
r.Get("/databases", s.DatabaseList)
|
||||
r.Get("/bloat", s.BloatList)
|
||||
r.Post("/maintenance", s.RunMaintenance)
|
||||
r.Get("/wal", s.WalStatus)
|
||||
r.Delete("/replication_slots/{slot}", s.DropReplicationSlot)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("postgresql")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取配置
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root))
|
||||
config, err := io.Read(s.configPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -68,13 +102,14 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root), req.Config, 0644); err != nil {
|
||||
oldPort := db.PostgresPort(app.Root)
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("postgresql"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload PostgreSQL: %v", err))
|
||||
if err = s.applyConfig(req.Config, oldPort); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to apply PostgreSQL config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -84,7 +119,7 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// GetUserConfig 获取用户配置
|
||||
func (s *App) GetUserConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取配置
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/postgresql/data/pg_hba.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -101,7 +136,7 @@ func (s *App) UpdateUserConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), req.Config, 0644); err != nil {
|
||||
if err = io.Write(app.Root+"/server/postgresql/data/pg_hba.conf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -128,17 +163,14 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.Setenv("PGPASSWORD", postgresPassword); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set PGPASSWORD env: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
start, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "select pg_postmaster_start_time();" | head -1 | cut -d'.' -f1`)
|
||||
env := []string{"PGPASSWORD=" + postgresPassword}
|
||||
port := db.PostgresPort(app.Root)
|
||||
start, err := shell.ExecfWithEnv(env, `psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_postmaster_start_time();" | head -1 | cut -d'.' -f1`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL start time: %v", err))
|
||||
return
|
||||
}
|
||||
pid, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "select pg_backend_pid();"`)
|
||||
pid, err := shell.ExecfWithEnv(env, `psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_backend_pid();"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL backend pid: %v", err))
|
||||
return
|
||||
@@ -148,22 +180,17 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL process: %v", err))
|
||||
return
|
||||
}
|
||||
connections, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();"`)
|
||||
connections, err := shell.ExecfWithEnv(env, `psql -h 127.0.0.1 -p %d -U postgres -t -c "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL connections: %v", err))
|
||||
return
|
||||
}
|
||||
storage, err := shell.Execf(`psql -h 127.0.0.1 -U postgres -t -c "select pg_size_pretty(pg_database_size('postgres'));"`)
|
||||
storage, err := shell.ExecfWithEnv(env, `psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_size_pretty(pg_database_size('postgres'));"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL database size: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.Unsetenv("PGPASSWORD"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to unset PGPASSWORD env: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Start Time"), Value: start},
|
||||
{Name: s.t.Get("Process PID"), Value: pid},
|
||||
@@ -175,19 +202,15 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// Log 获取日志
|
||||
// Log 获取应用日志路径列表
|
||||
func (s *App) Log(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, fmt.Sprintf("%s/server/postgresql/logs/postgresql-%s.log", app.Root, time.Now().Format(time.DateOnly)))
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *App) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf("rm -rf %s/server/postgresql/logs/postgresql-*.log", app.Root); err != nil {
|
||||
paths, err := filepath.Glob(app.Root + "/server/postgresql/logs/postgresql-*.log")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
service.Success(w, paths)
|
||||
}
|
||||
|
||||
// GetPostgresPassword 获取 postgres 用户密码
|
||||
@@ -210,10 +233,11 @@ func (s *App) SetPostgresPassword(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
oldPassword, _ := s.settingRepo.Get(biz.SettingKeyPostgresPassword)
|
||||
postgres, err := db.NewPostgres("postgres", oldPassword, "127.0.0.1", 5432)
|
||||
port := db.PostgresPort(app.Root)
|
||||
postgres, err := db.NewPostgres(r.Context(), "postgres", oldPassword, "127.0.0.1", port)
|
||||
if err != nil {
|
||||
// 直接修改密码
|
||||
if _, err = shell.Execf(`su - postgres -c "psql -c \"ALTER USER postgres WITH PASSWORD '%s';\""`, req.Password); err != nil {
|
||||
if _, err = shell.Execf(`su - postgres -c "psql -p %d -c \"ALTER USER postgres WITH PASSWORD '%s';\""`, port, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set postgres password: %v", err))
|
||||
return
|
||||
}
|
||||
@@ -237,7 +261,7 @@ func (s *App) SetPostgresPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetConfigTune 获取 PostgreSQL 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root))
|
||||
config, err := io.Read(s.configPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -245,32 +269,32 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tune := ConfigTune{
|
||||
// 连接设置
|
||||
ListenAddresses: s.getPGValue(config, "listen_addresses"),
|
||||
Port: s.getPGValue(config, "port"),
|
||||
MaxConnections: s.getPGValue(config, "max_connections"),
|
||||
SuperuserReservedConnections: s.getPGValue(config, "superuser_reserved_connections"),
|
||||
ListenAddresses: confval.Postgres.Get(config, "listen_addresses"),
|
||||
Port: confval.Postgres.Get(config, "port"),
|
||||
MaxConnections: confval.Postgres.Get(config, "max_connections"),
|
||||
SuperuserReservedConnections: confval.Postgres.Get(config, "superuser_reserved_connections"),
|
||||
// 内存设置
|
||||
SharedBuffers: s.getPGValue(config, "shared_buffers"),
|
||||
WorkMem: s.getPGValue(config, "work_mem"),
|
||||
MaintenanceWorkMem: s.getPGValue(config, "maintenance_work_mem"),
|
||||
EffectiveCacheSize: s.getPGValue(config, "effective_cache_size"),
|
||||
HugePages: s.getPGValue(config, "huge_pages"),
|
||||
SharedBuffers: confval.Postgres.Get(config, "shared_buffers"),
|
||||
WorkMem: confval.Postgres.Get(config, "work_mem"),
|
||||
MaintenanceWorkMem: confval.Postgres.Get(config, "maintenance_work_mem"),
|
||||
EffectiveCacheSize: confval.Postgres.Get(config, "effective_cache_size"),
|
||||
HugePages: confval.Postgres.Get(config, "huge_pages"),
|
||||
// WAL 设置
|
||||
WalLevel: s.getPGValue(config, "wal_level"),
|
||||
WalBuffers: s.getPGValue(config, "wal_buffers"),
|
||||
MaxWalSize: s.getPGValue(config, "max_wal_size"),
|
||||
MinWalSize: s.getPGValue(config, "min_wal_size"),
|
||||
CheckpointCompletionTarget: s.getPGValue(config, "checkpoint_completion_target"),
|
||||
WalLevel: confval.Postgres.Get(config, "wal_level"),
|
||||
WalBuffers: confval.Postgres.Get(config, "wal_buffers"),
|
||||
MaxWalSize: confval.Postgres.Get(config, "max_wal_size"),
|
||||
MinWalSize: confval.Postgres.Get(config, "min_wal_size"),
|
||||
CheckpointCompletionTarget: confval.Postgres.Get(config, "checkpoint_completion_target"),
|
||||
// 查询优化
|
||||
DefaultStatisticsTarget: s.getPGValue(config, "default_statistics_target"),
|
||||
RandomPageCost: s.getPGValue(config, "random_page_cost"),
|
||||
EffectiveIoConcurrency: s.getPGValue(config, "effective_io_concurrency"),
|
||||
DefaultStatisticsTarget: confval.Postgres.Get(config, "default_statistics_target"),
|
||||
RandomPageCost: confval.Postgres.Get(config, "random_page_cost"),
|
||||
EffectiveIoConcurrency: confval.Postgres.Get(config, "effective_io_concurrency"),
|
||||
// 日志设置
|
||||
LogDestination: s.getPGValue(config, "log_destination"),
|
||||
LogMinDurationStatement: s.getPGValue(config, "log_min_duration_statement"),
|
||||
LogTimezone: s.getPGValue(config, "log_timezone"),
|
||||
LogDestination: confval.Postgres.Get(config, "log_destination"),
|
||||
LogMinDurationStatement: confval.Postgres.Get(config, "log_min_duration_statement"),
|
||||
LogTimezone: confval.Postgres.Get(config, "log_timezone"),
|
||||
// IO 设置
|
||||
IoMethod: s.getPGValue(config, "io_method"),
|
||||
IoMethod: confval.Postgres.Get(config, "io_method"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
@@ -284,7 +308,8 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root)
|
||||
confPath := s.configPath()
|
||||
oldPort := db.PostgresPort(app.Root)
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -292,118 +317,597 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 更新连接设置
|
||||
config = s.setPGValue(config, "listen_addresses", req.ListenAddresses)
|
||||
config = s.setPGValue(config, "port", req.Port)
|
||||
config = s.setPGValue(config, "max_connections", req.MaxConnections)
|
||||
config = s.setPGValue(config, "superuser_reserved_connections", req.SuperuserReservedConnections)
|
||||
config = confval.Postgres.Set(config, "listen_addresses", req.ListenAddresses)
|
||||
config = confval.Postgres.Set(config, "port", req.Port)
|
||||
config = confval.Postgres.Set(config, "max_connections", req.MaxConnections)
|
||||
config = confval.Postgres.Set(config, "superuser_reserved_connections", req.SuperuserReservedConnections)
|
||||
// 更新内存设置
|
||||
config = s.setPGValue(config, "shared_buffers", req.SharedBuffers)
|
||||
config = s.setPGValue(config, "work_mem", req.WorkMem)
|
||||
config = s.setPGValue(config, "maintenance_work_mem", req.MaintenanceWorkMem)
|
||||
config = s.setPGValue(config, "effective_cache_size", req.EffectiveCacheSize)
|
||||
config = s.setPGValue(config, "huge_pages", req.HugePages)
|
||||
config = confval.Postgres.Set(config, "shared_buffers", req.SharedBuffers)
|
||||
config = confval.Postgres.Set(config, "work_mem", req.WorkMem)
|
||||
config = confval.Postgres.Set(config, "maintenance_work_mem", req.MaintenanceWorkMem)
|
||||
config = confval.Postgres.Set(config, "effective_cache_size", req.EffectiveCacheSize)
|
||||
config = confval.Postgres.Set(config, "huge_pages", req.HugePages)
|
||||
// 更新 WAL 设置
|
||||
config = s.setPGValue(config, "wal_level", req.WalLevel)
|
||||
config = s.setPGValue(config, "wal_buffers", req.WalBuffers)
|
||||
config = s.setPGValue(config, "max_wal_size", req.MaxWalSize)
|
||||
config = s.setPGValue(config, "min_wal_size", req.MinWalSize)
|
||||
config = s.setPGValue(config, "checkpoint_completion_target", req.CheckpointCompletionTarget)
|
||||
config = confval.Postgres.Set(config, "wal_level", req.WalLevel)
|
||||
config = confval.Postgres.Set(config, "wal_buffers", req.WalBuffers)
|
||||
config = confval.Postgres.Set(config, "max_wal_size", req.MaxWalSize)
|
||||
config = confval.Postgres.Set(config, "min_wal_size", req.MinWalSize)
|
||||
config = confval.Postgres.Set(config, "checkpoint_completion_target", req.CheckpointCompletionTarget)
|
||||
// 更新查询优化
|
||||
config = s.setPGValue(config, "default_statistics_target", req.DefaultStatisticsTarget)
|
||||
config = s.setPGValue(config, "random_page_cost", req.RandomPageCost)
|
||||
config = s.setPGValue(config, "effective_io_concurrency", req.EffectiveIoConcurrency)
|
||||
config = confval.Postgres.Set(config, "default_statistics_target", req.DefaultStatisticsTarget)
|
||||
config = confval.Postgres.Set(config, "random_page_cost", req.RandomPageCost)
|
||||
config = confval.Postgres.Set(config, "effective_io_concurrency", req.EffectiveIoConcurrency)
|
||||
// 更新日志设置
|
||||
config = s.setPGValue(config, "log_destination", req.LogDestination)
|
||||
config = s.setPGValue(config, "log_min_duration_statement", req.LogMinDurationStatement)
|
||||
config = s.setPGValue(config, "log_timezone", req.LogTimezone)
|
||||
config = confval.Postgres.Set(config, "log_destination", req.LogDestination)
|
||||
config = confval.Postgres.Set(config, "log_min_duration_statement", req.LogMinDurationStatement)
|
||||
config = confval.Postgres.Set(config, "log_timezone", req.LogTimezone)
|
||||
// 更新 IO 设置
|
||||
config = s.setPGValue(config, "io_method", req.IoMethod)
|
||||
config = confval.Postgres.Set(config, "io_method", req.IoMethod)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("postgresql"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload PostgreSQL: %v", err))
|
||||
if err = s.applyConfig(config, oldPort); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to apply PostgreSQL config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getPGValue 从 PostgreSQL 配置内容中获取指定键的值
|
||||
func (s *App) getPGValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
v := strings.TrimSpace(parts[1])
|
||||
// 去除行尾注释
|
||||
if idx := strings.Index(v, "#"); idx >= 0 {
|
||||
v = strings.TrimSpace(v[:idx])
|
||||
// ExtensionList 获取扩展列表及安装状态
|
||||
func (s *App) ExtensionList(w http.ResponseWriter, r *http.Request) {
|
||||
extensions := s.getExtensions()
|
||||
for i := range extensions {
|
||||
controlPath := fmt.Sprintf("%s/server/postgresql/share/extension/%s.control", app.Root, extensions[i].ExtName)
|
||||
extensions[i].Installed = io.Exists(controlPath)
|
||||
if extensions[i].Installed {
|
||||
control, _ := io.Read(controlPath)
|
||||
if m := defaultVersionRegexp.FindStringSubmatch(control); len(m) > 1 {
|
||||
extensions[i].InstalledVersion = m[1]
|
||||
}
|
||||
// 去除引号
|
||||
v = strings.Trim(v, "'\"")
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
service.Success(w, extensions)
|
||||
}
|
||||
|
||||
// setPGValue 在 PostgreSQL 配置内容中设置指定键的值
|
||||
func (s *App) setPGValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
// InstallExtension 安装扩展(异步任务)
|
||||
func (s *App) InstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExtensionSlug](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.SplitN(checkLine, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+line)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+" = '"+value+"'")
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
if !s.checkExtension(req.Slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("extension %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" = '"+value+"'")
|
||||
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/postgresql/extensions/%s.sh' | bash -s -- 'install'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = "postgresql:extension:" + req.Slug
|
||||
task.Name = s.t.Get("Install PostgreSQL extension %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UninstallExtension 卸载扩展(异步任务)
|
||||
func (s *App) UninstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExtensionSlug](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.checkExtension(req.Slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("extension %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/postgresql/extensions/%s.sh' | bash -s -- 'uninstall'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = "postgresql:extension:" + req.Slug
|
||||
task.Name = s.t.Get("Uninstall PostgreSQL extension %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// EnableExtension 在指定数据库启用扩展
|
||||
func (s *App) EnableExtension(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExtensionEnable](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ext, ok := lo.Find(s.getExtensions(), func(e Extension) bool {
|
||||
return e.Slug == req.Slug
|
||||
})
|
||||
if !ok {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("extension %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
postgres, err := s.connect(r.Context(), req.Database)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
if _, err = postgres.Exec(fmt.Sprintf(`CREATE EXTENSION IF NOT EXISTS "%s"`, ext.ExtName)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to enable extension: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// SessionList 获取会话列表
|
||||
func (s *App) SessionList(w http.ResponseWriter, r *http.Request) {
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
rows, err := postgres.Query(`
|
||||
SELECT a.pid, coalesce(a.datname,''), coalesce(a.usename,''), coalesce(a.client_addr::text,''),
|
||||
coalesce(a.state,''), coalesce(a.wait_event_type,''), coalesce(a.wait_event,''),
|
||||
coalesce(array_to_string(pg_blocking_pids(a.pid),','),''),
|
||||
coalesce(extract(epoch from (now()-a.xact_start))::bigint,0),
|
||||
coalesce(extract(epoch from (now()-a.query_start))::bigint,0),
|
||||
coalesce(a.query,'')
|
||||
FROM pg_stat_activity a
|
||||
WHERE a.pid != pg_backend_pid() AND a.backend_type = 'client backend'
|
||||
ORDER BY a.backend_start`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
sessions := make([]Session, 0)
|
||||
for rows.Next() {
|
||||
var item Session
|
||||
if err = rows.Scan(&item.PID, &item.Database, &item.User, &item.ClientAddr, &item.State,
|
||||
&item.WaitEventType, &item.WaitEvent, &item.BlockedBy, &item.XactSeconds, &item.QuerySeconds, &item.Query); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
sessions = append(sessions, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, sessions)
|
||||
}
|
||||
|
||||
// TerminateSession 终止会话
|
||||
func (s *App) TerminateSession(w http.ResponseWriter, r *http.Request) {
|
||||
pid := cast.ToInt64(chi.URLParam(r, "pid"))
|
||||
if pid <= 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid pid"))
|
||||
return
|
||||
}
|
||||
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
if _, err = postgres.Exec(`SELECT pg_terminate_backend($1)`, pid); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// TopSQL 获取 SQL 性能统计
|
||||
func (s *App) TopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
rows, err := postgres.Query(`
|
||||
SELECT coalesce(d.datname,''), s.calls, round(s.total_exec_time)::bigint,
|
||||
round(s.mean_exec_time::numeric,2)::float8, s.rows,
|
||||
coalesce(round(100.0*s.shared_blks_hit/nullif(s.shared_blks_hit+s.shared_blks_read,0),1),0)::float8,
|
||||
s.query
|
||||
FROM pg_stat_statements s LEFT JOIN pg_database d ON d.oid = s.dbid
|
||||
ORDER BY s.total_exec_time DESC LIMIT 50`)
|
||||
if err != nil {
|
||||
// pg_stat_statements 未启用时返回状态而非报错
|
||||
if strings.Contains(err.Error(), "shared_preload_libraries") {
|
||||
service.Success(w, TopSQL{Enabled: false, PendingRestart: true, Items: []TopSQLItem{}})
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
service.Success(w, TopSQL{Enabled: false, Items: []TopSQLItem{}})
|
||||
return
|
||||
}
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
items := make([]TopSQLItem, 0)
|
||||
for rows.Next() {
|
||||
var item TopSQLItem
|
||||
if err = rows.Scan(&item.Database, &item.Calls, &item.TotalMs, &item.MeanMs, &item.Rows, &item.HitRate, &item.Query); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, TopSQL{Enabled: true, Items: items})
|
||||
}
|
||||
|
||||
// EnableTopSQL 启用 pg_stat_statements
|
||||
func (s *App) EnableTopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(s.configPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 将 pg_stat_statements 加入 shared_preload_libraries,重启后生效
|
||||
var libs []string
|
||||
for lib := range strings.SplitSeq(confval.Postgres.Get(config, "shared_preload_libraries"), ",") {
|
||||
if lib = strings.TrimSpace(lib); lib != "" {
|
||||
libs = append(libs, lib)
|
||||
}
|
||||
}
|
||||
if !slices.Contains(libs, "pg_stat_statements") {
|
||||
libs = append(libs, "pg_stat_statements")
|
||||
config = confval.Postgres.Set(config, "shared_preload_libraries", strings.Join(libs, ","))
|
||||
if err = io.Write(s.configPath(), config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
if _, err = postgres.Exec(`CREATE EXTENSION IF NOT EXISTS pg_stat_statements`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ResetTopSQL 重置 SQL 性能统计
|
||||
func (s *App) ResetTopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
if _, err = postgres.Exec(`SELECT pg_stat_statements_reset()`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// DatabaseList 获取可连接的数据库列表
|
||||
func (s *App) DatabaseList(w http.ResponseWriter, r *http.Request) {
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
rows, err := postgres.Query(`SELECT datname FROM pg_database WHERE datallowconn ORDER BY datname`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
databases := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var name string
|
||||
if err = rows.Scan(&name); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
databases = append(databases, name)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, databases)
|
||||
}
|
||||
|
||||
// BloatList 获取指定数据库的表膨胀情况
|
||||
func (s *App) BloatList(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[BloatQuery](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
postgres, err := s.connect(r.Context(), req.Database)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
rows, err := postgres.Query(`
|
||||
SELECT schemaname, relname, pg_size_pretty(pg_total_relation_size(relid)),
|
||||
pg_total_relation_size(relid), n_live_tup, n_dead_tup,
|
||||
coalesce(round(100.0*n_dead_tup/nullif(n_live_tup+n_dead_tup,0),1),0)::float8,
|
||||
coalesce(to_char(last_vacuum,'YYYY-MM-DD HH24:MI'),''), coalesce(to_char(last_autovacuum,'YYYY-MM-DD HH24:MI'),''),
|
||||
coalesce(to_char(last_analyze,'YYYY-MM-DD HH24:MI'),''), coalesce(to_char(last_autoanalyze,'YYYY-MM-DD HH24:MI'),'')
|
||||
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 50`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
items := make([]BloatItem, 0)
|
||||
for rows.Next() {
|
||||
var item BloatItem
|
||||
if err = rows.Scan(&item.Schema, &item.Table, &item.Size, &item.SizeBytes, &item.LiveTuples, &item.DeadTuples,
|
||||
&item.DeadRate, &item.LastVacuum, &item.LastAutovacuum, &item.LastAnalyze, &item.LastAutoanalyz); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, Bloat{
|
||||
RepackInstalled: io.Exists(app.Root + "/server/postgresql/share/extension/pg_repack.control"),
|
||||
Items: items,
|
||||
})
|
||||
}
|
||||
|
||||
// RunMaintenance 对表执行维护操作(异步任务)
|
||||
func (s *App) RunMaintenance(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[MaintenanceRun](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Tables) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("no tables selected"))
|
||||
return
|
||||
}
|
||||
if !slices.Contains([]string{"vacuum", "vacuum_full", "analyze", "repack"}, req.Operation) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid operation"))
|
||||
return
|
||||
}
|
||||
|
||||
var cmd string
|
||||
if req.Operation == "repack" {
|
||||
if !io.Exists(app.Root + "/server/postgresql/share/extension/pg_repack.control") {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("pg_repack is not installed, please install it in the extensions tab first"))
|
||||
return
|
||||
}
|
||||
var args strings.Builder
|
||||
for _, table := range req.Tables {
|
||||
fmt.Fprintf(&args, ` -t "%s.%s"`, table.Schema, table.Table)
|
||||
}
|
||||
cmd = fmt.Sprintf(`su - postgres -c 'pg_repack -d "%s"%s'`, req.Database, args.String())
|
||||
} else {
|
||||
statement := map[string]string{"vacuum": "VACUUM", "vacuum_full": "VACUUM FULL", "analyze": "ANALYZE"}[req.Operation]
|
||||
var args strings.Builder
|
||||
for _, table := range req.Tables {
|
||||
fmt.Fprintf(&args, ` -c "%s \"%s\".\"%s\""`, statement, table.Schema, table.Table)
|
||||
}
|
||||
cmd = fmt.Sprintf(`su - postgres -c 'psql -d "%s"%s'`, req.Database, args.String())
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = "postgresql:maintenance:" + req.Database
|
||||
task.Name = s.t.Get("Run %[1]s on %[2]d tables in database %[3]s", req.Operation, len(req.Tables), req.Database)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// WalStatus 获取 WAL 及复制状态
|
||||
func (s *App) WalStatus(w http.ResponseWriter, r *http.Request) {
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
var wal Wal
|
||||
if err = postgres.QueryRow(`SELECT pg_size_pretty(coalesce(sum(size),0)) FROM pg_ls_waldir()`).Scan(&wal.WalSize); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = postgres.QueryRow(`SELECT archived_count, failed_count, coalesce(last_archived_wal,''), coalesce(last_failed_wal,'') FROM pg_stat_archiver`).Scan(
|
||||
&wal.Archiver.ArchivedCount, &wal.Archiver.FailedCount, &wal.Archiver.LastArchivedWal, &wal.Archiver.LastFailedWal); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
slotRows, err := postgres.Query(`SELECT slot_name, slot_type, active, coalesce(pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)),'') FROM pg_replication_slots`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = slotRows.Close() }()
|
||||
wal.Slots = make([]ReplicationSlot, 0)
|
||||
for slotRows.Next() {
|
||||
var slot ReplicationSlot
|
||||
if err = slotRows.Scan(&slot.Name, &slot.Type, &slot.Active, &slot.RetainedWal); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
wal.Slots = append(wal.Slots, slot)
|
||||
}
|
||||
if err = slotRows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
replRows, err := postgres.Query(`SELECT coalesce(client_addr::text,''), coalesce(state,''), coalesce(sync_state,''), coalesce(pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)),'') FROM pg_stat_replication`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = replRows.Close() }()
|
||||
wal.Replications = make([]Replication, 0)
|
||||
for replRows.Next() {
|
||||
var repl Replication
|
||||
if err = replRows.Scan(&repl.ClientAddr, &repl.State, &repl.SyncState, &repl.Lag); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
wal.Replications = append(wal.Replications, repl)
|
||||
}
|
||||
if err = replRows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, wal)
|
||||
}
|
||||
|
||||
// DropReplicationSlot 删除复制槽
|
||||
func (s *App) DropReplicationSlot(w http.ResponseWriter, r *http.Request) {
|
||||
slot := chi.URLParam(r, "slot")
|
||||
if slot == "" {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid slot name"))
|
||||
return
|
||||
}
|
||||
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
if _, err = postgres.Exec(`SELECT pg_drop_replication_slot($1)`, slot); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getExtensions 返回所有扩展定义
|
||||
func (s *App) getExtensions() []Extension {
|
||||
return []Extension{
|
||||
{Name: "pgvector", Slug: "pgvector", ExtName: "vector", Description: s.t.Get("Vector similarity search")},
|
||||
{Name: "PostGIS", Slug: "postgis", ExtName: "postgis", Description: s.t.Get("Spatial and geographic objects support")},
|
||||
{Name: "TimescaleDB", Slug: "timescaledb", ExtName: "timescaledb", Description: s.t.Get("Time-series database engine, requires restarting PostgreSQL after installation")},
|
||||
{Name: "zhparser", Slug: "zhparser", ExtName: "zhparser", Description: s.t.Get("Chinese full-text search parser based on SCWS")},
|
||||
{Name: "pg_repack", Slug: "pg_repack", ExtName: "pg_repack", Description: s.t.Get("Reorganize tables online to remove bloat")},
|
||||
{Name: "pg_cron", Slug: "pg_cron", ExtName: "pg_cron", Description: s.t.Get("Run periodic jobs inside the database, requires restarting PostgreSQL after installation")},
|
||||
{Name: "pg_partman", Slug: "pg_partman", ExtName: "pg_partman", Description: s.t.Get("Automated partition management")},
|
||||
{Name: "pgaudit", Slug: "pgaudit", ExtName: "pgaudit", Description: s.t.Get("Session and object audit logging, requires restarting PostgreSQL after installation")},
|
||||
{Name: "pg_hint_plan", Slug: "pg_hint_plan", ExtName: "pg_hint_plan", Description: s.t.Get("Control execution plans with hints in SQL comments, requires restarting PostgreSQL after installation")},
|
||||
{Name: "pg_stat_monitor", Slug: "pg_stat_monitor", ExtName: "pg_stat_monitor", Description: s.t.Get("Advanced query performance monitoring, requires restarting PostgreSQL after installation")},
|
||||
{Name: "pg_ivm", Slug: "pg_ivm", ExtName: "pg_ivm", Description: s.t.Get("Incremental view maintenance for materialized views")},
|
||||
{Name: "hypopg", Slug: "hypopg", ExtName: "hypopg", Description: s.t.Get("Hypothetical indexes for query plan testing")},
|
||||
{Name: "pgmq", Slug: "pgmq", ExtName: "pgmq", Description: s.t.Get("Lightweight message queue")},
|
||||
{Name: "orafce", Slug: "orafce", ExtName: "orafce", Description: s.t.Get("Oracle compatibility functions")},
|
||||
{Name: "http", Slug: "http", ExtName: "http", Description: s.t.Get("HTTP client for SQL, send requests from the database")},
|
||||
}
|
||||
}
|
||||
|
||||
// checkExtension 检查 slug 是否有效
|
||||
func (s *App) checkExtension(slug string) bool {
|
||||
return lo.ContainsBy(s.getExtensions(), func(e Extension) bool {
|
||||
return e.Slug == slug
|
||||
})
|
||||
}
|
||||
|
||||
// connect 以 postgres 超级用户连接指定数据库,默认 postgres 库
|
||||
func (s *App) connect(ctx context.Context, database ...string) (db.Operator, error) {
|
||||
password, err := s.settingRepo.Get(biz.SettingKeyPostgresPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.NewPostgres(ctx, "postgres", password, "127.0.0.1", db.PostgresPort(app.Root), database...)
|
||||
}
|
||||
|
||||
func (s *App) configPath() string {
|
||||
return app.Root + "/server/postgresql/data/postgresql.conf"
|
||||
}
|
||||
|
||||
// parsePort 从 config 内容解析端口,未配置时返回默认值
|
||||
func (s *App) parsePort(config string) uint {
|
||||
port := cast.ToUint(confval.Postgres.Get(config, "port"))
|
||||
if port == 0 {
|
||||
return 5432
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// applyConfig 让 PostgreSQL 配置生效
|
||||
func (s *App) applyConfig(newConfig string, oldPort uint) error {
|
||||
newPort := s.parsePort(newConfig)
|
||||
if oldPort == newPort {
|
||||
return systemctl.Reload("postgresql")
|
||||
}
|
||||
if err := systemctl.Restart("postgresql"); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.databaseServerRepo.UpdatePort("local_postgresql", newPort)
|
||||
}
|
||||
|
||||
@@ -5,14 +5,139 @@ type UpdateConfig struct {
|
||||
}
|
||||
|
||||
type SetPostgresPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
// Extension PostgreSQL 扩展信息
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"` // 下载脚本名,如 pgvector
|
||||
ExtName string `json:"ext_name"` // CREATE EXTENSION 使用的扩展名,如 vector
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
InstalledVersion string `json:"installed_version"` // control 文件中的 default_version,仅展示
|
||||
}
|
||||
|
||||
// ExtensionSlug 扩展操作请求
|
||||
type ExtensionSlug struct {
|
||||
Slug string `form:"slug" json:"slug" validate:"required"`
|
||||
}
|
||||
|
||||
// ExtensionEnable 在指定数据库启用扩展请求
|
||||
type ExtensionEnable struct {
|
||||
Slug string `form:"slug" json:"slug" validate:"required"`
|
||||
Database string `form:"database" json:"database" validate:"required"`
|
||||
}
|
||||
|
||||
// Session 数据库会话信息
|
||||
type Session struct {
|
||||
PID int64 `json:"pid"`
|
||||
Database string `json:"database"`
|
||||
User string `json:"user"`
|
||||
ClientAddr string `json:"client_addr"`
|
||||
State string `json:"state"`
|
||||
WaitEventType string `json:"wait_event_type"`
|
||||
WaitEvent string `json:"wait_event"`
|
||||
BlockedBy string `json:"blocked_by"`
|
||||
XactSeconds int64 `json:"xact_seconds"`
|
||||
QuerySeconds int64 `json:"query_seconds"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
// TopSQLItem Top SQL 统计项
|
||||
type TopSQLItem struct {
|
||||
Database string `json:"database"`
|
||||
Calls int64 `json:"calls"`
|
||||
TotalMs int64 `json:"total_ms"`
|
||||
MeanMs float64 `json:"mean_ms"`
|
||||
Rows int64 `json:"rows"`
|
||||
HitRate float64 `json:"hit_rate"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
// TopSQL Top SQL 响应
|
||||
type TopSQL struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PendingRestart bool `json:"pending_restart"` // 已配置 preload,等待重启生效
|
||||
Items []TopSQLItem `json:"items"`
|
||||
}
|
||||
|
||||
// BloatQuery 表膨胀查询请求
|
||||
type BloatQuery struct {
|
||||
Database string `form:"database" json:"database" validate:"required"`
|
||||
}
|
||||
|
||||
// BloatItem 表膨胀信息
|
||||
type BloatItem struct {
|
||||
Schema string `json:"schema"`
|
||||
Table string `json:"table"`
|
||||
Size string `json:"size"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
LiveTuples int64 `json:"live_tuples"`
|
||||
DeadTuples int64 `json:"dead_tuples"`
|
||||
DeadRate float64 `json:"dead_rate"`
|
||||
LastVacuum string `json:"last_vacuum"`
|
||||
LastAutovacuum string `json:"last_autovacuum"`
|
||||
LastAnalyze string `json:"last_analyze"`
|
||||
LastAutoanalyz string `json:"last_autoanalyze"`
|
||||
}
|
||||
|
||||
// Bloat 表膨胀响应
|
||||
type Bloat struct {
|
||||
RepackInstalled bool `json:"repack_installed"`
|
||||
Items []BloatItem `json:"items"`
|
||||
}
|
||||
|
||||
// MaintenanceTable 维护操作的目标表
|
||||
type MaintenanceTable struct {
|
||||
Schema string `form:"schema" json:"schema" validate:"required"`
|
||||
Table string `form:"table" json:"table" validate:"required"`
|
||||
}
|
||||
|
||||
// MaintenanceRun 表维护操作请求
|
||||
type MaintenanceRun struct {
|
||||
Database string `form:"database" json:"database" validate:"required"`
|
||||
Tables []MaintenanceTable `form:"tables" json:"tables"`
|
||||
Operation string `form:"operation" json:"operation" validate:"required"`
|
||||
}
|
||||
|
||||
// ReplicationSlot 复制槽信息
|
||||
type ReplicationSlot struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Active bool `json:"active"`
|
||||
RetainedWal string `json:"retained_wal"`
|
||||
}
|
||||
|
||||
// Replication 流复制连接信息
|
||||
type Replication struct {
|
||||
ClientAddr string `json:"client_addr"`
|
||||
State string `json:"state"`
|
||||
SyncState string `json:"sync_state"`
|
||||
Lag string `json:"lag"`
|
||||
}
|
||||
|
||||
// WalArchiver WAL 归档统计
|
||||
type WalArchiver struct {
|
||||
ArchivedCount int64 `json:"archived_count"`
|
||||
FailedCount int64 `json:"failed_count"`
|
||||
LastArchivedWal string `json:"last_archived_wal"`
|
||||
LastFailedWal string `json:"last_failed_wal"`
|
||||
}
|
||||
|
||||
// Wal WAL 状态响应
|
||||
type Wal struct {
|
||||
WalSize string `json:"wal_size"`
|
||||
Archiver WalArchiver `json:"archiver"`
|
||||
Slots []ReplicationSlot `json:"slots"`
|
||||
Replications []Replication `json:"replications"`
|
||||
}
|
||||
|
||||
// ConfigTune PostgreSQL 配置调整
|
||||
type ConfigTune struct {
|
||||
// 连接设置
|
||||
ListenAddresses string `form:"listen_addresses" json:"listen_addresses"`
|
||||
Port string `form:"port" json:"port"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections"`
|
||||
SuperuserReservedConnections string `form:"superuser_reserved_connections" json:"superuser_reserved_connections"`
|
||||
// 内存设置
|
||||
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
@@ -28,7 +30,8 @@ type App struct {
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, conf *config.Config, taskRepo biz.TaskRepo) *App {
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, taskRepo biz.TaskRepo) *App {
|
||||
|
||||
return &App{t: t, conf: conf, taskRepo: taskRepo}
|
||||
}
|
||||
|
||||
@@ -52,6 +55,12 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/exporters/{slug}/config", s.UpdateExporterConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
prom, _ := systemctl.Status("prometheus")
|
||||
alert, _ := systemctl.Status("alertmanager")
|
||||
return types.AggregateAppStatus(prom, alert)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("prometheus")
|
||||
if err != nil {
|
||||
@@ -66,7 +75,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get("http://127.0.0.1:9090/api/v1/status/runtimeinfo")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
if err != nil || !resp.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
@@ -97,33 +106,16 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root))
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, app.Root+"/server/prometheus/prometheus.yml")
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("prometheus"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, app.Root+"/server/prometheus/prometheus.yml", "prometheus")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Prometheus 全局配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root))
|
||||
conf, _ := io.Read(app.Root + "/server/prometheus/prometheus.yml")
|
||||
|
||||
var cfg struct {
|
||||
Global struct {
|
||||
@@ -151,7 +143,7 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root)
|
||||
confPath := app.Root + "/server/prometheus/prometheus.yml"
|
||||
raw, _ := io.Read(confPath)
|
||||
|
||||
var cfg map[string]any
|
||||
@@ -197,7 +189,7 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetAlertmanagerConfig 获取 Alertmanager 配置
|
||||
func (s *App) GetAlertmanagerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(fmt.Sprintf("%s/server/prometheus/alertmanager/alertmanager.yml", app.Root))
|
||||
conf, _ := io.Read(app.Root + "/server/prometheus/alertmanager/alertmanager.yml")
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
@@ -209,7 +201,7 @@ func (s *App) UpdateAlertmanagerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/prometheus/alertmanager/alertmanager.yml", app.Root), req.Config, 0644); err != nil {
|
||||
if err = io.Write(app.Root+"/server/prometheus/alertmanager/alertmanager.yml", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -252,6 +244,7 @@ func (s *App) InstallExporter(w http.ResponseWriter, r *http.Request) {
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/prometheus/exporters/exporter.sh' | bash -s -- 'install' '%s'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = "prometheus:exporter:" + req.Slug
|
||||
task.Name = s.t.Get("Install Prometheus exporter %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
@@ -279,6 +272,7 @@ func (s *App) UninstallExporter(w http.ResponseWriter, r *http.Request) {
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/prometheus/exporters/exporter.sh' | bash -s -- 'uninstall' '%s'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = "prometheus:exporter:" + req.Slug
|
||||
task.Name = s.t.Get("Uninstall Prometheus exporter %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
@@ -420,10 +414,7 @@ func (s *App) getExporterConfigPath(slug string) string {
|
||||
|
||||
// checkExporter 检查 slug 是否有效
|
||||
func (s *App) checkExporter(slug string) bool {
|
||||
for _, e := range s.getExporters() {
|
||||
if e.Slug == slug {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return lo.ContainsBy(s.getExporters(), func(e Exporter) bool {
|
||||
return e.Slug == slug
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,15 +8,18 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/firewall"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -24,6 +27,7 @@ type App struct {
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
@@ -40,6 +44,11 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("pure-ftpd")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
listRaw, err := shell.Execf("pure-pw list")
|
||||
@@ -50,19 +59,17 @@ func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
listArr := strings.Split(listRaw, "\n")
|
||||
var users []User
|
||||
for _, v := range listArr {
|
||||
userRe := regexp.MustCompile(`(\S+)\s+(\S+)`)
|
||||
users := lo.FilterMap(strings.Split(listRaw, "\n"), func(v string, _ int) (User, bool) {
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
return User{}, false
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v)
|
||||
users = append(users, User{
|
||||
match := userRe.FindStringSubmatch(v)
|
||||
return User{
|
||||
Username: match[1],
|
||||
Path: strings.Replace(match[2], "/./", "/", 1),
|
||||
})
|
||||
}
|
||||
}, true
|
||||
})
|
||||
|
||||
paged, total := service.Paginate(r, users)
|
||||
|
||||
@@ -142,13 +149,13 @@ func (s *App) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetPort 获取端口
|
||||
func (s *App) GetPort(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/pure-ftpd/etc/pure-ftpd.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/pure-ftpd/etc/pure-ftpd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get port: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
bind := strings.Trim(s.getFTPValue(config, "Bind"), `"'`)
|
||||
bind := strings.Trim(confval.FTP.Get(config, "Bind"), `"'`)
|
||||
port := 21 // 默认端口
|
||||
if parts := strings.SplitN(bind, ",", 2); len(parts) == 2 {
|
||||
port = cast.ToInt(strings.TrimSpace(parts[1]))
|
||||
@@ -165,13 +172,13 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/pure-ftpd/etc/pure-ftpd.conf", app.Root)
|
||||
confPath := app.Root + "/server/pure-ftpd/etc/pure-ftpd.conf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
config = s.setFTPValue(config, "Bind", fmt.Sprintf(`"0.0.0.0,%d"`, req.Port))
|
||||
config = confval.FTP.Set(config, "Bind", fmt.Sprintf(`"0.0.0.0,%d"`, req.Port))
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -200,21 +207,21 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetConfigTune 获取 Pure-FTPd 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/pure-ftpd/etc/pure-ftpd.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/pure-ftpd/etc/pure-ftpd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
MaxClientsNumber: s.getFTPValue(config, "MaxClientsNumber"),
|
||||
MaxClientsPerIP: s.getFTPValue(config, "MaxClientsPerIP"),
|
||||
MaxIdleTime: s.getFTPValue(config, "MaxIdleTime"),
|
||||
MaxLoad: s.getFTPValue(config, "MaxLoad"),
|
||||
PassivePortRange: s.getFTPValue(config, "PassivePortRange"),
|
||||
AnonymousOnly: s.getFTPValue(config, "AnonymousOnly"),
|
||||
NoAnonymous: s.getFTPValue(config, "NoAnonymous"),
|
||||
MaxDiskUsage: s.getFTPValue(config, "MaxDiskUsage"),
|
||||
MaxClientsNumber: confval.FTP.Get(config, "MaxClientsNumber"),
|
||||
MaxClientsPerIP: confval.FTP.Get(config, "MaxClientsPerIP"),
|
||||
MaxIdleTime: confval.FTP.Get(config, "MaxIdleTime"),
|
||||
MaxLoad: confval.FTP.Get(config, "MaxLoad"),
|
||||
PassivePortRange: confval.FTP.Get(config, "PassivePortRange"),
|
||||
AnonymousOnly: confval.FTP.Get(config, "AnonymousOnly"),
|
||||
NoAnonymous: confval.FTP.Get(config, "NoAnonymous"),
|
||||
MaxDiskUsage: confval.FTP.Get(config, "MaxDiskUsage"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
@@ -228,21 +235,21 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/pure-ftpd/etc/pure-ftpd.conf", app.Root)
|
||||
confPath := app.Root + "/server/pure-ftpd/etc/pure-ftpd.conf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config = s.setFTPValue(config, "MaxClientsNumber", req.MaxClientsNumber)
|
||||
config = s.setFTPValue(config, "MaxClientsPerIP", req.MaxClientsPerIP)
|
||||
config = s.setFTPValue(config, "MaxIdleTime", req.MaxIdleTime)
|
||||
config = s.setFTPValue(config, "MaxLoad", req.MaxLoad)
|
||||
config = s.setFTPValue(config, "PassivePortRange", req.PassivePortRange)
|
||||
config = s.setFTPValue(config, "AnonymousOnly", req.AnonymousOnly)
|
||||
config = s.setFTPValue(config, "NoAnonymous", req.NoAnonymous)
|
||||
config = s.setFTPValue(config, "MaxDiskUsage", req.MaxDiskUsage)
|
||||
config = confval.FTP.Set(config, "MaxClientsNumber", req.MaxClientsNumber)
|
||||
config = confval.FTP.Set(config, "MaxClientsPerIP", req.MaxClientsPerIP)
|
||||
config = confval.FTP.Set(config, "MaxIdleTime", req.MaxIdleTime)
|
||||
config = confval.FTP.Set(config, "MaxLoad", req.MaxLoad)
|
||||
config = confval.FTP.Set(config, "PassivePortRange", req.PassivePortRange)
|
||||
config = confval.FTP.Set(config, "AnonymousOnly", req.AnonymousOnly)
|
||||
config = confval.FTP.Set(config, "NoAnonymous", req.NoAnonymous)
|
||||
config = confval.FTP.Set(config, "MaxDiskUsage", req.MaxDiskUsage)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -256,65 +263,3 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getFTPValue 从 Pure-FTPd 配置内容中获取指定键的值
|
||||
func (s *App) getFTPValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
return strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setFTPValue 在 Pure-FTPd 配置内容中设置指定键的值
|
||||
func (s *App) setFTPValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.Fields(checkLine)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+line)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 保留原行格式
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
result = append(result, indent+key+" "+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" "+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package pureftpd
|
||||
|
||||
type Create struct {
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -12,21 +12,21 @@ type Delete struct {
|
||||
|
||||
type ChangePassword struct {
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required|password"`
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port" validate:"required|number|min:1|max:65535"`
|
||||
Port uint `form:"port" json:"port" validate:"required && number && min:1 && max:65535"`
|
||||
}
|
||||
|
||||
// ConfigTune Pure-FTPd 配置调整
|
||||
type ConfigTune struct {
|
||||
MaxClientsNumber string `form:"max_clients_number" json:"max_clients_number"`
|
||||
MaxClientsPerIP string `form:"max_clients_per_ip" json:"max_clients_per_ip"`
|
||||
MaxIdleTime string `form:"max_idle_time" json:"max_idle_time"`
|
||||
MaxClientsNumber string `form:"max_clients_number" json:"max_clients_number" validate:"number && min:1"`
|
||||
MaxClientsPerIP string `form:"max_clients_per_ip" json:"max_clients_per_ip" validate:"number && min:1"`
|
||||
MaxIdleTime string `form:"max_idle_time" json:"max_idle_time" validate:"number && min:0"`
|
||||
MaxLoad string `form:"max_load" json:"max_load"`
|
||||
PassivePortRange string `form:"passive_port_range" json:"passive_port_range"`
|
||||
AnonymousOnly string `form:"anonymous_only" json:"anonymous_only"`
|
||||
NoAnonymous string `form:"no_anonymous" json:"no_anonymous"`
|
||||
MaxDiskUsage string `form:"max_disk_usage" json:"max_disk_usage"`
|
||||
AnonymousOnly string `form:"anonymous_only" json:"anonymous_only" validate:"in:yes,no"`
|
||||
NoAnonymous string `form:"no_anonymous" json:"no_anonymous" validate:"in:yes,no"`
|
||||
MaxDiskUsage string `form:"max_disk_usage" json:"max_disk_usage" validate:"number && min:1 && max:100"`
|
||||
}
|
||||
|
||||
+313
-111
@@ -1,32 +1,52 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
redigo "github.com/gomodule/redigo/redis"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
taskRepo biz.TaskRepo
|
||||
slug string // 服务名与配置目录名,如 redis、valkey
|
||||
name string // 展示名,如 Redis、Valkey
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, taskRepo biz.TaskRepo) *App {
|
||||
return New("redis", "Redis", t, databaseServerRepo, taskRepo)
|
||||
}
|
||||
|
||||
func New(slug, name string, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, taskRepo biz.TaskRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
databaseServerRepo: databaseServer,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
taskRepo: taskRepo,
|
||||
slug: slug,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,12 +56,24 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
// 性能诊断
|
||||
r.Get("/slow_log", s.SlowLog)
|
||||
r.Post("/slow_log/reset", s.ResetSlowLog)
|
||||
r.Get("/clients", s.ClientList)
|
||||
r.Post("/clients/kill", s.KillClient)
|
||||
r.Get("/memory", s.MemoryStatus)
|
||||
r.Post("/bigkeys", s.ScanBigKeys)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status(s.slug)
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("redis")
|
||||
status, err := systemctl.Status(s.slug)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get redis status: %v", err))
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get %s status: %v", s.name, err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
@@ -49,9 +81,9 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 Redis 密码
|
||||
// 检查密码
|
||||
withPassword := ""
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/redis/redis.conf", app.Root))
|
||||
config, err := io.Read(s.confPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -62,21 +94,19 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
withPassword = " -a " + matches[1]
|
||||
}
|
||||
|
||||
raw, err := shell.Execf("redis-cli%s info", withPassword)
|
||||
raw, err := shell.Execf("%s%s info", s.slug+"-cli", withPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get redis info: %v", err))
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get %s info: %v", s.name, err))
|
||||
return
|
||||
}
|
||||
|
||||
infoLines := strings.Split(raw, "\n")
|
||||
dataRaw := make(map[string]string)
|
||||
|
||||
for _, item := range infoLines {
|
||||
dataRaw := lo.SliceToMap(strings.Split(raw, "\n"), func(item string) (string, string) {
|
||||
parts := strings.Split(item, ":")
|
||||
if len(parts) == 2 {
|
||||
dataRaw[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
if len(parts) != 2 {
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
||||
})
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("TCP Port"), Value: dataRaw["tcp_port"]},
|
||||
@@ -98,60 +128,38 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/redis/redis.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, s.confPath())
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/redis/redis.conf", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("redis"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.confPath(), s.slug)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Redis 配置调整参数
|
||||
// GetConfigTune 获取配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/redis/redis.conf", app.Root))
|
||||
config, err := io.Read(s.confPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
Bind: s.getRedisValue(config, "bind"),
|
||||
Port: s.getRedisValue(config, "port"),
|
||||
Databases: s.getRedisValue(config, "databases"),
|
||||
Requirepass: s.getRedisValue(config, "requirepass"),
|
||||
Timeout: s.getRedisValue(config, "timeout"),
|
||||
TCPKeepalive: s.getRedisValue(config, "tcp-keepalive"),
|
||||
Maxmemory: s.getRedisValue(config, "maxmemory"),
|
||||
MaxmemoryPolicy: s.getRedisValue(config, "maxmemory-policy"),
|
||||
Appendonly: s.getRedisValue(config, "appendonly"),
|
||||
Appendfsync: s.getRedisValue(config, "appendfsync"),
|
||||
Bind: confval.Directive.Get(config, "bind"),
|
||||
Port: confval.Directive.Get(config, "port"),
|
||||
Databases: confval.Directive.Get(config, "databases"),
|
||||
Requirepass: confval.Directive.Get(config, "requirepass"),
|
||||
Timeout: confval.Directive.Get(config, "timeout"),
|
||||
TCPKeepalive: confval.Directive.Get(config, "tcp-keepalive"),
|
||||
Maxmemory: confval.Directive.Get(config, "maxmemory"),
|
||||
MaxmemoryPolicy: confval.Directive.Get(config, "maxmemory-policy"),
|
||||
Appendonly: confval.Directive.Get(config, "appendonly"),
|
||||
Appendfsync: confval.Directive.Get(config, "appendfsync"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Redis 配置调整参数
|
||||
// UpdateConfigTune 更新配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
@@ -159,96 +167,290 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/redis/redis.conf", app.Root)
|
||||
confPath := s.confPath()
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config = s.setRedisValue(config, "bind", req.Bind)
|
||||
config = s.setRedisValue(config, "port", req.Port)
|
||||
config = s.setRedisValue(config, "databases", req.Databases)
|
||||
config = s.setRedisValue(config, "requirepass", req.Requirepass)
|
||||
config = s.setRedisValue(config, "timeout", req.Timeout)
|
||||
config = s.setRedisValue(config, "tcp-keepalive", req.TCPKeepalive)
|
||||
config = s.setRedisValue(config, "maxmemory", req.Maxmemory)
|
||||
config = s.setRedisValue(config, "maxmemory-policy", req.MaxmemoryPolicy)
|
||||
config = s.setRedisValue(config, "appendonly", req.Appendonly)
|
||||
config = s.setRedisValue(config, "appendfsync", req.Appendfsync)
|
||||
config = confval.Directive.Set(config, "bind", req.Bind)
|
||||
config = confval.Directive.Set(config, "port", req.Port)
|
||||
config = confval.Directive.Set(config, "databases", req.Databases)
|
||||
config = confval.Directive.Set(config, "requirepass", req.Requirepass)
|
||||
config = confval.Directive.Set(config, "timeout", req.Timeout)
|
||||
config = confval.Directive.Set(config, "tcp-keepalive", req.TCPKeepalive)
|
||||
config = confval.Directive.Set(config, "maxmemory", req.Maxmemory)
|
||||
config = confval.Directive.Set(config, "maxmemory-policy", req.MaxmemoryPolicy)
|
||||
config = confval.Directive.Set(config, "appendonly", req.Appendonly)
|
||||
config = confval.Directive.Set(config, "appendfsync", req.Appendfsync)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("redis"); err != nil {
|
||||
if err = systemctl.Restart(s.slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 同步密码到数据库服务器记录
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_redis", req.Requirepass)
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_"+s.slug, req.Requirepass)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getRedisValue 从 Redis 配置内容中获取指定键的值
|
||||
func (s *App) getRedisValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
// SlowLog 获取慢日志
|
||||
func (s *App) SlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
reply, err := conn.Exec("SLOWLOG", "GET", 50)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
rows, err := redigo.Values(reply, nil)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
entries := make([]SlowLogEntry, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
item, itemErr := redigo.Values(row, nil)
|
||||
if itemErr != nil || len(item) < 4 {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
return strings.Join(parts[1:], " ")
|
||||
entry := SlowLogEntry{}
|
||||
entry.ID, _ = redigo.Int64(item[0], nil)
|
||||
if ts, tsErr := redigo.Int64(item[1], nil); tsErr == nil {
|
||||
entry.Time = time.Unix(ts, 0).Format(time.DateTime)
|
||||
}
|
||||
entry.DurationUs, _ = redigo.Int64(item[2], nil)
|
||||
if cmd, cmdErr := redigo.Strings(item[3], nil); cmdErr == nil {
|
||||
entry.Command = strings.Join(cmd, " ")
|
||||
}
|
||||
if len(item) > 4 {
|
||||
entry.Client, _ = redigo.String(item[4], nil)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return ""
|
||||
|
||||
service.Success(w, entries)
|
||||
}
|
||||
|
||||
// setRedisValue 在 Redis 配置内容中设置指定键的值
|
||||
func (s *App) setRedisValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
// ResetSlowLog 重置慢日志
|
||||
func (s *App) ResetSlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
if _, err = conn.Exec("SLOWLOG", "RESET"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ClientList 获取客户端连接列表
|
||||
func (s *App) ClientList(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
raw, err := redigo.String(conn.Exec("CLIENT", "LIST"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
clients := make([]Client, 0)
|
||||
for line := range strings.SplitSeq(strings.TrimSpace(raw), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.Fields(checkLine)
|
||||
if len(parts) >= 1 && parts[0] == key {
|
||||
if found {
|
||||
continue
|
||||
fields := map[string]string{}
|
||||
for kv := range strings.FieldsSeq(line) {
|
||||
if key, value, found := strings.Cut(kv, "="); found {
|
||||
fields[key] = value
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "# "+trimmed)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+" "+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
clients = append(clients, Client{
|
||||
ID: fields["id"],
|
||||
Addr: fields["addr"],
|
||||
Name: fields["name"],
|
||||
DB: fields["db"],
|
||||
Age: fields["age"],
|
||||
Idle: fields["idle"],
|
||||
Cmd: fields["cmd"],
|
||||
})
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" "+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
|
||||
service.Success(w, clients)
|
||||
}
|
||||
|
||||
// KillClient 踢除客户端连接
|
||||
func (s *App) KillClient(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ClientKill](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
killed, err := redigo.Int64(conn.Exec("CLIENT", "KILL", "ID", req.ID))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if killed == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("client %d not found", req.ID))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// MemoryStatus 获取内存诊断信息
|
||||
func (s *App) MemoryStatus(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
memory := Memory{Items: make([]types.NV, 0)}
|
||||
memory.Doctor, err = redigo.String(conn.Exec("MEMORY", "DOCTOR"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
// 空库与无问题时返回的是电影梗彩蛋文案,无展示价值
|
||||
if strings.HasPrefix(memory.Doctor, "Hi Sam,") || strings.Contains(memory.Doctor, "can't find any memory issue") {
|
||||
memory.Doctor = ""
|
||||
}
|
||||
|
||||
// 未限制内存上限时给出预警
|
||||
if maxmemory, mmErr := redigo.Strings(conn.Exec("CONFIG", "GET", "maxmemory")); mmErr == nil && len(maxmemory) == 2 && maxmemory[1] == "0" {
|
||||
memory.NoLimit = true
|
||||
}
|
||||
|
||||
reply, err := conn.Exec("MEMORY", "STATS")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
values, err := redigo.Values(reply, nil)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
stats := map[string]string{}
|
||||
for i := 0; i+1 < len(values); i += 2 {
|
||||
key, keyErr := redigo.String(values[i], nil)
|
||||
if keyErr != nil {
|
||||
continue
|
||||
}
|
||||
// 值可能为整数或字符串,嵌套数组(如 db.0)跳过
|
||||
if v, vErr := redigo.Int64(values[i+1], nil); vErr == nil {
|
||||
stats[key] = cast.ToString(v)
|
||||
continue
|
||||
}
|
||||
if v, vErr := redigo.String(values[i+1], nil); vErr == nil {
|
||||
stats[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
// 各版本键名存在差异,仅展示存在的指标
|
||||
items := []struct {
|
||||
key string
|
||||
name string
|
||||
bytes bool
|
||||
}{
|
||||
{"peak.allocated", s.t.Get("Peak Allocated"), true},
|
||||
{"total.allocated", s.t.Get("Total Allocated"), true},
|
||||
{"startup.allocated", s.t.Get("Startup Allocated"), true},
|
||||
{"dataset.bytes", s.t.Get("Dataset Size"), true},
|
||||
{"dataset.percentage", s.t.Get("Dataset Percentage"), false},
|
||||
{"keys.count", s.t.Get("Keys Count"), false},
|
||||
{"keys.bytes-per-key", s.t.Get("Bytes Per Key"), true},
|
||||
{"allocator-fragmentation.ratio", s.t.Get("Allocator Fragmentation Ratio"), false},
|
||||
{"fragmentation", s.t.Get("Fragmentation Ratio"), false},
|
||||
}
|
||||
for _, item := range items {
|
||||
value, ok := stats[item.key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if item.bytes {
|
||||
value = tools.FormatBytes(cast.ToFloat64(value))
|
||||
}
|
||||
memory.Items = append(memory.Items, types.NV{Name: item.name, Value: value})
|
||||
}
|
||||
|
||||
service.Success(w, memory)
|
||||
}
|
||||
|
||||
// ScanBigKeys 扫描大 Key(异步任务)
|
||||
func (s *App) ScanBigKeys(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(s.confPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
withPassword := ""
|
||||
if password := confval.Directive.Get(config, "requirepass"); password != "" {
|
||||
withPassword = " -a " + password
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = s.slug + ":bigkeys"
|
||||
task.Name = s.t.Get("Scan %s big keys", s.name)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf("%s-cli%s --bigkeys", s.slug, withPassword)
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// connect 从配置文件读取端口与密码建立连接
|
||||
func (s *App) connect(ctx context.Context) (*db.Redis, error) {
|
||||
config, err := io.Read(s.confPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port := confval.Directive.Get(config, "port")
|
||||
if port == "" {
|
||||
port = "6379"
|
||||
}
|
||||
password := confval.Directive.Get(config, "requirepass")
|
||||
|
||||
return db.NewRedis(ctx, "", password, "127.0.0.1:"+port)
|
||||
}
|
||||
|
||||
func (s *App) confPath() string {
|
||||
return filepath.Join(app.Root, "server", s.slug, s.slug+".conf")
|
||||
}
|
||||
|
||||
@@ -1,22 +1,52 @@
|
||||
package redis
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
import "github.com/acepanel/panel/v3/pkg/types"
|
||||
|
||||
// ConfigTune Redis 配置调整
|
||||
// ConfigTune Redis 协议兼容服务的配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
Bind string `form:"bind" json:"bind"`
|
||||
Port string `form:"port" json:"port"`
|
||||
Databases string `form:"databases" json:"databases"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
Databases string `form:"databases" json:"databases" validate:"number && min:1"`
|
||||
Requirepass string `form:"requirepass" json:"requirepass"`
|
||||
Timeout string `form:"timeout" json:"timeout"`
|
||||
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive"`
|
||||
Timeout string `form:"timeout" json:"timeout" validate:"number"`
|
||||
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive" validate:"number"`
|
||||
// 内存
|
||||
Maxmemory string `form:"maxmemory" json:"maxmemory"`
|
||||
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy"`
|
||||
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy" validate:"in:noeviction,allkeys-lru,allkeys-lfu,allkeys-random,volatile-lru,volatile-lfu,volatile-random,volatile-ttl"`
|
||||
// 持久化
|
||||
Appendonly string `form:"appendonly" json:"appendonly"`
|
||||
Appendfsync string `form:"appendfsync" json:"appendfsync"`
|
||||
Appendonly string `form:"appendonly" json:"appendonly" validate:"in:yes,no"`
|
||||
Appendfsync string `form:"appendfsync" json:"appendfsync" validate:"in:always,everysec,no"`
|
||||
}
|
||||
|
||||
// ClientKill 踢除客户端连接请求
|
||||
type ClientKill struct {
|
||||
ID int64 `form:"id" json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
// SlowLogEntry 慢日志条目
|
||||
type SlowLogEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
Time string `json:"time"`
|
||||
DurationUs int64 `json:"duration_us"`
|
||||
Command string `json:"command"`
|
||||
Client string `json:"client"`
|
||||
}
|
||||
|
||||
// Client 客户端连接信息
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
Addr string `json:"addr"`
|
||||
Name string `json:"name"`
|
||||
DB string `json:"db"`
|
||||
Age string `json:"age"`
|
||||
Idle string `json:"idle"`
|
||||
Cmd string `json:"cmd"`
|
||||
}
|
||||
|
||||
// Memory 内存诊断信息
|
||||
type Memory struct {
|
||||
Doctor string `json:"doctor"`
|
||||
NoLimit bool `json:"no_limit"`
|
||||
Items []types.NV `json:"items"`
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -21,6 +21,7 @@ type App struct {
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
@@ -32,6 +33,12 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
namesrv, _ := systemctl.Status("rocketmq-namesrv")
|
||||
broker, _ := systemctl.Status("rocketmq-broker")
|
||||
return types.AggregateAppStatus(namesrv, broker)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
namesrvStatus, _ := systemctl.Status("rocketmq-namesrv")
|
||||
brokerStatus, _ := systemctl.Status("rocketmq-broker")
|
||||
@@ -55,11 +62,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("NameServer Status"), Value: namesrvStr},
|
||||
{Name: s.t.Get("Broker Status"), Value: brokerStr},
|
||||
{Name: s.t.Get("Broker Name"), Value: s.getPropertiesValue(config, "brokerName")},
|
||||
{Name: s.t.Get("Listen Port"), Value: s.getPropertiesValue(config, "listenPort")},
|
||||
{Name: s.t.Get("NameServer Address"), Value: s.getPropertiesValue(config, "namesrvAddr")},
|
||||
{Name: s.t.Get("Broker Role"), Value: s.getPropertiesValue(config, "brokerRole")},
|
||||
{Name: s.t.Get("Flush Disk Type"), Value: s.getPropertiesValue(config, "flushDiskType")},
|
||||
{Name: s.t.Get("Broker Name"), Value: confval.Properties.Get(config, "brokerName")},
|
||||
{Name: s.t.Get("Listen Port"), Value: confval.Properties.Get(config, "listenPort")},
|
||||
{Name: s.t.Get("NameServer Address"), Value: confval.Properties.Get(config, "namesrvAddr")},
|
||||
{Name: s.t.Get("Broker Role"), Value: confval.Properties.Get(config, "brokerRole")},
|
||||
{Name: s.t.Get("Flush Disk Type"), Value: confval.Properties.Get(config, "flushDiskType")},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
@@ -99,14 +106,14 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
brokerInit, brokerMax := s.parseHeapLine(heapRaw, "ROCKETMQ_BROKER_HEAP")
|
||||
|
||||
tune := ConfigTune{
|
||||
BrokerName: s.getPropertiesValue(config, "brokerName"),
|
||||
ListenPort: s.getPropertiesValue(config, "listenPort"),
|
||||
NamesrvAddr: s.getPropertiesValue(config, "namesrvAddr"),
|
||||
BrokerRole: s.getPropertiesValue(config, "brokerRole"),
|
||||
FlushDiskType: s.getPropertiesValue(config, "flushDiskType"),
|
||||
StorePathRootDir: s.getPropertiesValue(config, "storePathRootDir"),
|
||||
StorePathCommitLog: s.getPropertiesValue(config, "storePathCommitLog"),
|
||||
MaxMessageSize: s.getPropertiesValue(config, "maxMessageSize"),
|
||||
BrokerName: confval.Properties.Get(config, "brokerName"),
|
||||
ListenPort: confval.Properties.Get(config, "listenPort"),
|
||||
NamesrvAddr: confval.Properties.Get(config, "namesrvAddr"),
|
||||
BrokerRole: confval.Properties.Get(config, "brokerRole"),
|
||||
FlushDiskType: confval.Properties.Get(config, "flushDiskType"),
|
||||
StorePathRootDir: confval.Properties.Get(config, "storePathRootDir"),
|
||||
StorePathCommitLog: confval.Properties.Get(config, "storePathCommitLog"),
|
||||
MaxMessageSize: confval.Properties.Get(config, "maxMessageSize"),
|
||||
NamesrvHeapInitSize: namesrvInit,
|
||||
NamesrvHeapMaxSize: namesrvMax,
|
||||
BrokerHeapInitSize: brokerInit,
|
||||
@@ -126,14 +133,14 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
config = s.setPropertiesValue(config, "brokerName", req.BrokerName)
|
||||
config = s.setPropertiesValue(config, "listenPort", req.ListenPort)
|
||||
config = s.setPropertiesValue(config, "namesrvAddr", req.NamesrvAddr)
|
||||
config = s.setPropertiesValue(config, "brokerRole", req.BrokerRole)
|
||||
config = s.setPropertiesValue(config, "flushDiskType", req.FlushDiskType)
|
||||
config = s.setPropertiesValue(config, "storePathRootDir", req.StorePathRootDir)
|
||||
config = s.setPropertiesValue(config, "storePathCommitLog", req.StorePathCommitLog)
|
||||
config = s.setPropertiesValue(config, "maxMessageSize", req.MaxMessageSize)
|
||||
config = confval.Properties.Set(config, "brokerName", req.BrokerName)
|
||||
config = confval.Properties.Set(config, "listenPort", req.ListenPort)
|
||||
config = confval.Properties.Set(config, "namesrvAddr", req.NamesrvAddr)
|
||||
config = confval.Properties.Set(config, "brokerRole", req.BrokerRole)
|
||||
config = confval.Properties.Set(config, "flushDiskType", req.FlushDiskType)
|
||||
config = confval.Properties.Set(config, "storePathRootDir", req.StorePathRootDir)
|
||||
config = confval.Properties.Set(config, "storePathCommitLog", req.StorePathCommitLog)
|
||||
config = confval.Properties.Set(config, "maxMessageSize", req.MaxMessageSize)
|
||||
|
||||
if err = io.Write(s.configPath(), config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -168,70 +175,12 @@ func (s *App) restartServices() error {
|
||||
|
||||
// configPath 返回 broker 配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/rocketmq/conf/broker.conf", app.Root)
|
||||
return app.Root + "/server/rocketmq/conf/broker.conf"
|
||||
}
|
||||
|
||||
// heapEnvPath 返回 JVM 堆内存配置文件路径
|
||||
func (s *App) heapEnvPath() string {
|
||||
return fmt.Sprintf("%s/server/rocketmq/conf/heap.env", app.Root)
|
||||
}
|
||||
|
||||
// getPropertiesValue 从 properties 内容中获取指定键的值
|
||||
func (s *App) getPropertiesValue(content string, key string) string {
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(trimmed, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setPropertiesValue 在 properties 内容中设置指定键的值
|
||||
func (s *App) setPropertiesValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
k, _, ok := strings.Cut(checkLine, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+trimmed)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+"="+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+"="+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
return app.Root + "/server/rocketmq/conf/heap.env"
|
||||
}
|
||||
|
||||
// parseHeapLine 从 heap.env 中提取指定环境变量的堆内存配置
|
||||
|
||||
@@ -8,15 +8,15 @@ type UpdateConfig struct {
|
||||
// ConfigTune RocketMQ 配置调整
|
||||
type ConfigTune struct {
|
||||
// Broker 基础
|
||||
BrokerName string `form:"broker_name" json:"broker_name"` // brokerName
|
||||
ListenPort string `form:"listen_port" json:"listen_port"` // listenPort
|
||||
NamesrvAddr string `form:"namesrv_addr" json:"namesrv_addr"` // namesrvAddr
|
||||
BrokerRole string `form:"broker_role" json:"broker_role"` // brokerRole
|
||||
FlushDiskType string `form:"flush_disk_type" json:"flush_disk_type"` // flushDiskType
|
||||
BrokerName string `form:"broker_name" json:"broker_name"` // brokerName
|
||||
ListenPort string `form:"listen_port" json:"listen_port" validate:"number && min:1 && max:65535"` // listenPort
|
||||
NamesrvAddr string `form:"namesrv_addr" json:"namesrv_addr"` // namesrvAddr
|
||||
BrokerRole string `form:"broker_role" json:"broker_role" validate:"in:ASYNC_MASTER,SYNC_MASTER,SLAVE"` // brokerRole
|
||||
FlushDiskType string `form:"flush_disk_type" json:"flush_disk_type" validate:"in:ASYNC_FLUSH,SYNC_FLUSH"` // flushDiskType
|
||||
// 存储
|
||||
StorePathRootDir string `form:"store_path_root_dir" json:"store_path_root_dir"` // storePathRootDir
|
||||
StorePathCommitLog string `form:"store_path_commit_log" json:"store_path_commit_log"` // storePathCommitLog
|
||||
MaxMessageSize string `form:"max_message_size" json:"max_message_size"` // maxMessageSize
|
||||
StorePathRootDir string `form:"store_path_root_dir" json:"store_path_root_dir" validate:"unix_path"` // storePathRootDir
|
||||
StorePathCommitLog string `form:"store_path_commit_log" json:"store_path_commit_log" validate:"unix_path"` // storePathCommitLog
|
||||
MaxMessageSize string `form:"max_message_size" json:"max_message_size" validate:"number"` // maxMessageSize
|
||||
// JVM - NameServer
|
||||
NamesrvHeapInitSize string `form:"namesrv_heap_init_size" json:"namesrv_heap_init_size"` // -Xms (namesrv)
|
||||
NamesrvHeapMaxSize string `form:"namesrv_heap_max_size" json:"namesrv_heap_max_size"` // -Xmx (namesrv)
|
||||
|
||||
+30
-174
@@ -1,20 +1,17 @@
|
||||
package rsync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/utils/str"
|
||||
"github.com/libtnb/chix/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -22,6 +19,7 @@ type App struct {
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
@@ -36,63 +34,18 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("rsyncd")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
modules, err := listModules()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var modules []Module
|
||||
lines := strings.Split(config, "\n")
|
||||
var currentModule *Module
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
if currentModule != nil {
|
||||
modules = append(modules, *currentModule)
|
||||
}
|
||||
moduleName := line[1 : len(line)-1]
|
||||
currentModule = &Module{
|
||||
Name: moduleName,
|
||||
}
|
||||
} else if currentModule != nil {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
switch key {
|
||||
case "path":
|
||||
currentModule.Path = value
|
||||
case "comment":
|
||||
currentModule.Comment = value
|
||||
case "read only":
|
||||
currentModule.ReadOnly = value == "yes" || value == "true"
|
||||
case "auth users":
|
||||
currentModule.AuthUser = value
|
||||
currentModule.Secret, err = shell.Execf(`grep -E '^%s:.*$' /etc/rsyncd.secrets | awk -F ':' '{print $2}'`, currentModule.AuthUser)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get the secret key for module %s", currentModule.AuthUser))
|
||||
return
|
||||
}
|
||||
case "hosts allow":
|
||||
currentModule.HostsAllow = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentModule != nil {
|
||||
modules = append(modules, *currentModule)
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, modules)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
@@ -102,136 +55,47 @@ func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Create](r)
|
||||
req, err := service.Bind[Module](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if strings.Contains(config, "["+req.Name+"]") {
|
||||
if io.Exists(modulePath(req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("module %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
conf := `# ` + req.Name + `-START
|
||||
[` + req.Name + `]
|
||||
path = ` + req.Path + `
|
||||
comment = ` + req.Comment + `
|
||||
read only = no
|
||||
auth users = ` + req.AuthUser + `
|
||||
hosts allow = ` + req.HostsAllow + `
|
||||
secrets file = /etc/rsyncd.secrets
|
||||
# ` + req.Name + `-END
|
||||
`
|
||||
|
||||
if err = io.WriteAppend("/etc/rsyncd.conf", conf, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.WriteAppend("/etc/rsyncd.secrets", fmt.Sprintf("%s:%s\n", req.AuthUser, req.Secret), 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Delete](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(config, "["+req.Name+"]") {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("module %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
module := str.Cut(config, "# "+req.Name+"-START", "# "+req.Name+"-END")
|
||||
config = strings.ReplaceAll(config, "\n# "+req.Name+"-START"+module+"# "+req.Name+"-END", "")
|
||||
|
||||
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
|
||||
if len(match) == 2 {
|
||||
authUser := match[1]
|
||||
if _, err = shell.Execf(`sed -i '/^%s:.*$/d' /etc/rsyncd.secrets`, authUser); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
s.save(w, req)
|
||||
}
|
||||
|
||||
func (s *App) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Update](r)
|
||||
req, err := service.Bind[Module](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(config, "["+req.Name+"]") {
|
||||
if !io.Exists(modulePath(req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("module %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
newConf := `# ` + req.Name + `-START
|
||||
[` + req.Name + `]
|
||||
path = ` + req.Path + `
|
||||
comment = ` + req.Comment + `
|
||||
read only = no
|
||||
auth users = ` + req.AuthUser + `
|
||||
hosts allow = ` + req.HostsAllow + `
|
||||
secrets file = /etc/rsyncd.secrets
|
||||
# ` + req.Name + `-END`
|
||||
s.save(w, req)
|
||||
}
|
||||
|
||||
module := str.Cut(config, "# "+req.Name+"-START", "# "+req.Name+"-END")
|
||||
config = strings.ReplaceAll(config, "# "+req.Name+"-START"+module+"# "+req.Name+"-END", newConf)
|
||||
|
||||
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
|
||||
if len(match) == 2 {
|
||||
authUser := match[1]
|
||||
if _, err = shell.Execf(`sed -i '/^%s:.*$/d' /etc/rsyncd.secrets`, authUser); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ModuleName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil {
|
||||
if err = io.Remove(modulePath(req.Name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.WriteAppend("/etc/rsyncd.secrets", fmt.Sprintf("%s:%s\n", req.AuthUser, req.Secret), 0600); err != nil {
|
||||
if err = io.Remove(secretsPath(req.Name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -245,28 +109,20 @@ secrets file = /etc/rsyncd.secrets
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, rsyncdConf)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
common.SaveConfig(w, r, rsyncdConf, "rsyncd")
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/rsyncd.conf", req.Config, 0644); err != nil {
|
||||
func (s *App) save(w http.ResponseWriter, req *Module) {
|
||||
if err := writeModule(*req); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
if err := systemctl.Restart("rsyncd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
package rsync
|
||||
|
||||
type Create struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
|
||||
Secret string `form:"secret" json:"secret" validate:"required"`
|
||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
|
||||
Secret string `form:"secret" json:"secret" validate:"required"`
|
||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||
}
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
type ModuleName struct {
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package rsync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
)
|
||||
|
||||
const (
|
||||
rsyncdConf = "/etc/rsyncd.conf"
|
||||
rsyncdDir = "/etc/rsyncd.d"
|
||||
)
|
||||
|
||||
func modulePath(name string) string {
|
||||
return filepath.Join(rsyncdDir, name+".conf")
|
||||
}
|
||||
|
||||
// secretsPath 一个模块独立的密钥文件,.secrets 后缀不会被 rsyncd 当作模块读入
|
||||
func secretsPath(name string) string {
|
||||
return filepath.Join(rsyncdDir, name+".secrets")
|
||||
}
|
||||
|
||||
func readModule(name string) (Module, error) {
|
||||
config, err := io.Read(modulePath(name))
|
||||
if err != nil {
|
||||
return Module{}, err
|
||||
}
|
||||
|
||||
secret, _ := io.Read(secretsPath(name))
|
||||
_, secret, _ = strings.Cut(strings.TrimSpace(secret), ":")
|
||||
|
||||
return Module{
|
||||
Name: name,
|
||||
Path: confval.SectionINI.GetIn(config, name, "path"),
|
||||
Comment: confval.SectionINI.GetIn(config, name, "comment"),
|
||||
ReadOnly: confval.SectionINI.GetIn(config, name, "read only") == "yes",
|
||||
AuthUser: confval.SectionINI.GetIn(config, name, "auth users"),
|
||||
HostsAllow: confval.SectionINI.GetIn(config, name, "hosts allow"),
|
||||
Secret: secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func listModules() ([]Module, error) {
|
||||
// Glob 在目录不存在时返回空,无需额外判断
|
||||
paths, err := filepath.Glob(modulePath("*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modules := make([]Module, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
module, err := readModule(strings.TrimSuffix(filepath.Base(path), ".conf"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modules = append(modules, module)
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
func writeModule(module Module) error {
|
||||
if err := os.MkdirAll(rsyncdDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := fmt.Sprintf(
|
||||
"[%s]\npath = %s\ncomment = %s\nread only = %s\nauth users = %s\nhosts allow = %s\nsecrets file = %s\n",
|
||||
module.Name, module.Path, module.Comment, lo.Ternary(module.ReadOnly, "yes", "no"),
|
||||
module.AuthUser, module.HostsAllow, secretsPath(module.Name),
|
||||
)
|
||||
if err := io.Write(modulePath(module.Name), config, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return io.Write(secretsPath(module.Name), module.AuthUser+":"+module.Secret+"\n", 0600)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package rsync
|
||||
|
||||
type Module struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
Path string `json:"path" validate:"required && unix_path"`
|
||||
Comment string `json:"comment"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
AuthUser string `json:"auth_user"`
|
||||
Secret string `json:"secret"`
|
||||
AuthUser string `json:"auth_user" validate:"required"`
|
||||
Secret string `json:"secret" validate:"required"`
|
||||
HostsAllow string `json:"hosts_allow"`
|
||||
}
|
||||
|
||||
+13
-14
@@ -9,12 +9,14 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -22,6 +24,7 @@ type App struct {
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
@@ -33,6 +36,10 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Delete("/mounts", s.Delete)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
return types.AppStatusNA
|
||||
}
|
||||
|
||||
// List 所有 S3fs 挂载
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := s.mounts()
|
||||
@@ -81,11 +88,9 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range list {
|
||||
if item.Path == req.Path {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount path already exists"))
|
||||
return
|
||||
}
|
||||
if lo.ContainsBy(list, func(item Mount) bool { return item.Path == req.Path }) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount path already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
id := time.Now().UnixMicro()
|
||||
@@ -128,14 +133,8 @@ func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var mount Mount
|
||||
for _, item := range list {
|
||||
if item.ID == req.ID {
|
||||
mount = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if mount.ID == 0 {
|
||||
mount, ok := lo.Find(list, func(item Mount) bool { return item.ID == req.ID })
|
||||
if !ok {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("mount not found"))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ type Create struct {
|
||||
Ak string `form:"ak" json:"ak" validate:"required"`
|
||||
Sk string `form:"sk" json:"sk" validate:"required"`
|
||||
Bucket string `form:"bucket" json:"bucket" validate:"required"`
|
||||
URL string `form:"url" json:"url" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
URL string `form:"url" json:"url" validate:"required && url"`
|
||||
Path string `form:"path" json:"path" validate:"required && unix_path"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
|
||||
+127
-158
@@ -6,14 +6,18 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/os"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -22,22 +26,14 @@ type App struct {
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
var name string
|
||||
if os.IsRHEL() {
|
||||
name = "supervisord"
|
||||
} else {
|
||||
name = "supervisor"
|
||||
}
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
name: name,
|
||||
name: lo.Ternary(os.IsRHEL(), "supervisord", "supervisor"),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/service", s.Service)
|
||||
r.Post("/clear_log", s.ClearLog)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/processes", s.Processes)
|
||||
@@ -45,11 +41,12 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/processes/{process}/stop", s.StopProcess)
|
||||
r.Post("/processes/{process}/restart", s.RestartProcess)
|
||||
r.Get("/processes/{process}/log", s.ProcessLog)
|
||||
r.Post("/processes/{process}/clear_log", s.ClearProcessLog)
|
||||
r.Get("/processes/{process}", s.ProcessConfig)
|
||||
r.Post("/processes/{process}", s.UpdateProcessConfig)
|
||||
r.Delete("/processes/{process}", s.DeleteProcess)
|
||||
r.Get("/processes/{process}/setting", s.GetProcessSetting)
|
||||
r.Post("/processes/{process}/setting", s.UpdateProcessSetting)
|
||||
r.Post("/processes", s.CreateProcess)
|
||||
r.Delete("/processes/{process}", s.DeleteProcess)
|
||||
}
|
||||
|
||||
// Service 获取服务名称
|
||||
@@ -57,88 +54,52 @@ func (s *App) Service(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, s.name)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *App) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf(`cat /dev/null > /var/log/supervisor/supervisord.log`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status(s.name)
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var config string
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
config, err = io.Read(`/etc/supervisord.conf`)
|
||||
} else {
|
||||
config, err = io.Read(`/etc/supervisor/supervisord.conf`)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, mainConfPath())
|
||||
}
|
||||
|
||||
// UpdateConfig 保存配置
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.conf`, req.Config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/supervisord.conf`, req.Config, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart(s.name); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to restart %s: %v", s.name, err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, mainConfPath(), s.name)
|
||||
}
|
||||
|
||||
// Processes 进程列表
|
||||
func (s *App) Processes(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := shell.Execf(`supervisorctl status | awk '{print $1}'`)
|
||||
if err != nil {
|
||||
out, err := shell.Execf(`supervisorctl status`)
|
||||
if err != nil && out == "" {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var processes []Process
|
||||
for line := range strings.SplitSeq(out, "\n") {
|
||||
if len(line) == 0 {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
var p Process
|
||||
p.Name = line
|
||||
if status, err := shell.Execf(`supervisorctl status '%s' | awk '{print $2}'`, line); err == nil {
|
||||
p.Status = status
|
||||
p := Process{
|
||||
Name: fields[0],
|
||||
Status: fields[1],
|
||||
Pid: "-",
|
||||
Uptime: "-",
|
||||
}
|
||||
// RUNNING 行格式:name RUNNING pid 1234, uptime 1:23:45 (超过 1 天为 "1 day, 1:23:45")
|
||||
if p.Status == "RUNNING" {
|
||||
pid, _ := shell.Execf(`supervisorctl status '%s' | awk '{print $4}'`, line)
|
||||
p.Pid = strings.ReplaceAll(pid, ",", "")
|
||||
uptime, _ := shell.Execf(`supervisorctl status '%s' | awk '{print $6}'`, line)
|
||||
p.Uptime = uptime
|
||||
} else {
|
||||
p.Pid = "-"
|
||||
p.Uptime = "-"
|
||||
if _, rest, ok := strings.Cut(line, "pid "); ok {
|
||||
if pid, _, ok := strings.Cut(rest, ","); ok {
|
||||
p.Pid = pid
|
||||
}
|
||||
}
|
||||
if _, uptime, ok := strings.Cut(line, "uptime "); ok {
|
||||
p.Uptime = uptime
|
||||
}
|
||||
}
|
||||
processes = append(processes, p)
|
||||
}
|
||||
@@ -159,7 +120,7 @@ func (s *App) StartProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl start %s`, req.Process); err != nil {
|
||||
if out, err := shell.Execf(`supervisorctl start '%s'`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
@@ -175,7 +136,7 @@ func (s *App) StopProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl stop %s`, req.Process); err != nil {
|
||||
if out, err := shell.Execf(`supervisorctl stop '%s'`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
@@ -191,7 +152,7 @@ func (s *App) RestartProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl restart %s`, req.Process); err != nil {
|
||||
if out, err := shell.Execf(`supervisorctl restart '%s'`, req.Process); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
@@ -207,13 +168,7 @@ func (s *App) ProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
}
|
||||
|
||||
logPath, err := s.processLog(req.Process)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
@@ -222,35 +177,7 @@ func (s *App) ProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, logPath)
|
||||
}
|
||||
|
||||
// ClearProcessLog 清空进程日志
|
||||
func (s *App) ClearProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(`cat /dev/null > '%s'`, logPath); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ProcessConfig 获取进程配置
|
||||
// ProcessConfig 获取进程配置原文
|
||||
func (s *App) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
@@ -258,13 +185,7 @@ func (s *App) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var config string
|
||||
if os.IsRHEL() {
|
||||
config, err = io.Read(`/etc/supervisord.d/` + req.Process + `.conf`)
|
||||
} else {
|
||||
config, err = io.Read(`/etc/supervisor/conf.d/` + req.Process + `.conf`)
|
||||
}
|
||||
|
||||
config, err := io.Read(confPath(programName(req.Process)))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -273,7 +194,7 @@ func (s *App) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// UpdateProcessConfig 保存进程配置
|
||||
// UpdateProcessConfig 保存进程配置原文
|
||||
func (s *App) UpdateProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateProcessConfig](r)
|
||||
if err != nil {
|
||||
@@ -281,22 +202,57 @@ func (s *App) UpdateProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.d/`+req.Process+`.conf`, req.Config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+req.Process+`.conf`, req.Config, 0644)
|
||||
name := programName(req.Process)
|
||||
if err = io.Write(confPath(name), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
s.reload(w, name)
|
||||
}
|
||||
|
||||
// GetProcessSetting 获取进程可视化参数
|
||||
func (s *App) GetProcessSetting(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl restart '%s'`, req.Process)
|
||||
config, err := io.Read(confPath(programName(req.Process)))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
setting := new(ProcessSetting)
|
||||
readSetting(config, setting)
|
||||
|
||||
service.Success(w, setting)
|
||||
}
|
||||
|
||||
// UpdateProcessSetting 保存进程可视化参数
|
||||
func (s *App) UpdateProcessSetting(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateProcessSetting](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
name := programName(req.Process)
|
||||
path := confPath(name)
|
||||
config, err := io.Read(path)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(path, writeSetting(config, &req.ProcessSetting), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
s.reload(w, name)
|
||||
}
|
||||
|
||||
// CreateProcess 添加进程
|
||||
@@ -307,33 +263,34 @@ func (s *App) CreateProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
path := confPath(req.Name)
|
||||
if io.Exists(path) {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("process %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
num := cast.ToString(req.Num)
|
||||
config := `[program:` + req.Name + `]
|
||||
command=` + req.Command + `
|
||||
process_name=%(program_name)s
|
||||
process_name=` + processNameExpr(num) + `
|
||||
directory=` + req.Path + `
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=` + req.User + `
|
||||
numprocs=` + cast.ToString(req.Num) + `
|
||||
numprocs=` + num + `
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/` + req.Name + `.log
|
||||
stdout_logfile_maxbytes=2MB
|
||||
`
|
||||
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.d/`+req.Name+`.conf`, config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+req.Name+`.conf`, config, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err = io.Write(path, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl start '%s'`, req.Name)
|
||||
_, _ = shell.Execf(`supervisorctl start '%s:'`, req.Name)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -346,40 +303,52 @@ func (s *App) DeleteProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if out, err := shell.Execf(`supervisorctl stop '%s'`, req.Process); err != nil {
|
||||
name := programName(req.Process)
|
||||
if out, err := shell.Execf(`supervisorctl stop '%s:'`, name); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v %s", err, out)
|
||||
return
|
||||
}
|
||||
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
if err = io.Remove(`/etc/supervisord.d/` + req.Process + `.conf`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, req.Process)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
if err = io.Remove(`/etc/supervisor/conf.d/` + req.Process + `.conf`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
logPath, err := s.processLog(req.Process)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Remove(confPath(name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Remove(logPath); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) processLog(process string) (string, error) {
|
||||
config, err := io.Read(confPath(programName(process)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return confval.Supervisor.Get(config, "stdout_logfile"), nil
|
||||
}
|
||||
|
||||
func (s *App) reload(w http.ResponseWriter, name string) {
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl restart '%s:'`, name)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// programName 从 supervisorctl 的显示名还原出程序名,program 名本身不允许含 ":",切分无歧义
|
||||
func programName(process string) string {
|
||||
name, _, _ := strings.Cut(process, ":")
|
||||
return name
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/pkg/os"
|
||||
)
|
||||
|
||||
// mainConfPath supervisor 主配置,RHEL 系与 Debian 系的路径不同
|
||||
func mainConfPath() string {
|
||||
return lo.Ternary(os.IsRHEL(), "/etc/supervisord.conf", "/etc/supervisor/supervisord.conf")
|
||||
}
|
||||
|
||||
func confPath(name string) string {
|
||||
return lo.Ternary(os.IsRHEL(), "/etc/supervisord.d/", "/etc/supervisor/conf.d/") + name + ".conf"
|
||||
}
|
||||
|
||||
type settingField struct {
|
||||
key string
|
||||
value *string
|
||||
}
|
||||
|
||||
func settingFields(s *ProcessSetting) []settingField {
|
||||
return []settingField{
|
||||
{"command", &s.Command},
|
||||
{"directory", &s.Directory},
|
||||
{"user", &s.User},
|
||||
{"numprocs", &s.NumProcs},
|
||||
{"priority", &s.Priority},
|
||||
{"autostart", &s.AutoStart},
|
||||
{"autorestart", &s.AutoRestart},
|
||||
{"startsecs", &s.StartSecs},
|
||||
{"startretries", &s.StartRetries},
|
||||
{"stopwaitsecs", &s.StopWaitSecs},
|
||||
{"stopasgroup", &s.StopAsGroup},
|
||||
{"killasgroup", &s.KillAsGroup},
|
||||
{"redirect_stderr", &s.RedirectStderr},
|
||||
{"stdout_logfile", &s.StdoutLogfile},
|
||||
{"stdout_logfile_maxbytes", &s.StdoutLogfileMaxBytes},
|
||||
{"stdout_logfile_backups", &s.StdoutLogfileBackups},
|
||||
{"environment", &s.Environment},
|
||||
}
|
||||
}
|
||||
|
||||
func readSetting(config string, setting *ProcessSetting) {
|
||||
for _, f := range settingFields(setting) {
|
||||
*f.value = confval.Supervisor.Get(config, f.key)
|
||||
}
|
||||
}
|
||||
|
||||
// writeSetting numprocs 大于 1 时 process_name 必须带上 process_num,否则 supervisor 会拒绝加载
|
||||
func writeSetting(config string, setting *ProcessSetting) string {
|
||||
for _, f := range settingFields(setting) {
|
||||
config = confval.Supervisor.Set(config, f.key, *f.value)
|
||||
}
|
||||
|
||||
return confval.Supervisor.Set(config, "process_name", processNameExpr(setting.NumProcs))
|
||||
}
|
||||
|
||||
func processNameExpr(numProcs string) string {
|
||||
return lo.Ternary(numProcs == "" || numProcs == "1", `%(program_name)s`, `%(program_name)s_%(process_num)02d`)
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
package supervisor
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateProcessConfig struct {
|
||||
Process string `form:"process" json:"process" validate:"required"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
@@ -13,10 +9,15 @@ type ProcessName struct {
|
||||
Process string `form:"process" json:"process" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateProcessSetting struct {
|
||||
Process string `form:"process" json:"process" validate:"required"`
|
||||
ProcessSetting
|
||||
}
|
||||
|
||||
type CreateProcess struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
User string `form:"user" json:"user" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required && unix_path"`
|
||||
Command string `form:"command" json:"command" validate:"required"`
|
||||
Num int `form:"num" json:"num" validate:"required|min:1"`
|
||||
Num int `form:"num" json:"num" validate:"required && min:1"`
|
||||
}
|
||||
|
||||
@@ -6,3 +6,24 @@ type Process struct {
|
||||
Pid string `json:"pid"`
|
||||
Uptime string `json:"uptime"`
|
||||
}
|
||||
|
||||
// ProcessSetting 进程的可视化参数,留空表示注释掉该项、走 supervisor 默认值
|
||||
type ProcessSetting struct {
|
||||
Command string `form:"command" json:"command" validate:"required"`
|
||||
Directory string `form:"directory" json:"directory"`
|
||||
User string `form:"user" json:"user"`
|
||||
NumProcs string `form:"numprocs" json:"numprocs"`
|
||||
Priority string `form:"priority" json:"priority"`
|
||||
AutoStart string `form:"autostart" json:"autostart"`
|
||||
AutoRestart string `form:"autorestart" json:"autorestart"`
|
||||
StartSecs string `form:"startsecs" json:"startsecs"`
|
||||
StartRetries string `form:"startretries" json:"startretries"`
|
||||
StopWaitSecs string `form:"stopwaitsecs" json:"stopwaitsecs"`
|
||||
StopAsGroup string `form:"stopasgroup" json:"stopasgroup"`
|
||||
KillAsGroup string `form:"killasgroup" json:"killasgroup"`
|
||||
RedirectStderr string `form:"redirect_stderr" json:"redirect_stderr"`
|
||||
StdoutLogfile string `form:"stdout_logfile" json:"stdout_logfile"`
|
||||
StdoutLogfileMaxBytes string `form:"stdout_logfile_maxbytes" json:"stdout_logfile_maxbytes"`
|
||||
StdoutLogfileBackups string `form:"stdout_logfile_backups" json:"stdout_logfile_backups"`
|
||||
Environment string `form:"environment" json:"environment"`
|
||||
}
|
||||
|
||||
+7
-233
@@ -1,253 +1,27 @@
|
||||
package valkey
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
redis *redis.App
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServer biz.DatabaseServerRepo) *App {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, taskRepo biz.TaskRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
databaseServerRepo: databaseServer,
|
||||
redis: redis.New("valkey", "Valkey", t, databaseServerRepo, taskRepo),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
s.redis.Route(r)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("valkey")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get valkey status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 Valkey 密码
|
||||
withPassword := ""
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
re := regexp.MustCompile(`(?m)^requirepass\s+(.+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) == 2 {
|
||||
withPassword = " -a " + matches[1]
|
||||
}
|
||||
|
||||
raw, err := shell.Execf("valkey-cli%s info", withPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get valkey info: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
infoLines := strings.Split(raw, "\n")
|
||||
dataRaw := make(map[string]string)
|
||||
|
||||
for _, item := range infoLines {
|
||||
parts := strings.Split(item, ":")
|
||||
if len(parts) == 2 {
|
||||
dataRaw[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("TCP Port"), Value: dataRaw["tcp_port"]},
|
||||
{Name: s.t.Get("Uptime in Days"), Value: dataRaw["uptime_in_days"]},
|
||||
{Name: s.t.Get("Connected Clients"), Value: dataRaw["connected_clients"]},
|
||||
{Name: s.t.Get("Total Allocated Memory"), Value: dataRaw["used_memory_human"]},
|
||||
{Name: s.t.Get("Total Memory Usage"), Value: dataRaw["used_memory_rss_human"]},
|
||||
{Name: s.t.Get("Peak Memory Usage"), Value: dataRaw["used_memory_peak_human"]},
|
||||
{Name: s.t.Get("Memory Fragmentation Ratio"), Value: dataRaw["mem_fragmentation_ratio"]},
|
||||
{Name: s.t.Get("Total Connections Received"), Value: dataRaw["total_connections_received"]},
|
||||
{Name: s.t.Get("Total Commands Processed"), Value: dataRaw["total_commands_processed"]},
|
||||
{Name: s.t.Get("Commands Per Second"), Value: dataRaw["instantaneous_ops_per_sec"]},
|
||||
{Name: s.t.Get("Keyspace Hits"), Value: dataRaw["keyspace_hits"]},
|
||||
{Name: s.t.Get("Keyspace Misses"), Value: dataRaw["keyspace_misses"]},
|
||||
{Name: s.t.Get("Latest Fork Time (ms)"), Value: dataRaw["latest_fork_usec"]},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("valkey"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Valkey 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
Bind: s.getValkeyValue(config, "bind"),
|
||||
Port: s.getValkeyValue(config, "port"),
|
||||
Databases: s.getValkeyValue(config, "databases"),
|
||||
Requirepass: s.getValkeyValue(config, "requirepass"),
|
||||
Timeout: s.getValkeyValue(config, "timeout"),
|
||||
TCPKeepalive: s.getValkeyValue(config, "tcp-keepalive"),
|
||||
Maxmemory: s.getValkeyValue(config, "maxmemory"),
|
||||
MaxmemoryPolicy: s.getValkeyValue(config, "maxmemory-policy"),
|
||||
Appendonly: s.getValkeyValue(config, "appendonly"),
|
||||
Appendfsync: s.getValkeyValue(config, "appendfsync"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Valkey 配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root)
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config = s.setValkeyValue(config, "bind", req.Bind)
|
||||
config = s.setValkeyValue(config, "port", req.Port)
|
||||
config = s.setValkeyValue(config, "databases", req.Databases)
|
||||
config = s.setValkeyValue(config, "requirepass", req.Requirepass)
|
||||
config = s.setValkeyValue(config, "timeout", req.Timeout)
|
||||
config = s.setValkeyValue(config, "tcp-keepalive", req.TCPKeepalive)
|
||||
config = s.setValkeyValue(config, "maxmemory", req.Maxmemory)
|
||||
config = s.setValkeyValue(config, "maxmemory-policy", req.MaxmemoryPolicy)
|
||||
config = s.setValkeyValue(config, "appendonly", req.Appendonly)
|
||||
config = s.setValkeyValue(config, "appendfsync", req.Appendfsync)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("valkey"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 同步密码到数据库服务器记录
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_valkey", req.Requirepass)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getValkeyValue 从 Valkey 配置内容中获取指定键的值
|
||||
func (s *App) getValkeyValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
return strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setValkeyValue 在 Valkey 配置内容中设置指定键的值
|
||||
func (s *App) setValkeyValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.Fields(checkLine)
|
||||
if len(parts) >= 1 && parts[0] == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "# "+trimmed)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+" "+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" "+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
func (s *App) Status() string {
|
||||
return s.redis.Status()
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package valkey
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Valkey 配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
Bind string `form:"bind" json:"bind"`
|
||||
Port string `form:"port" json:"port"`
|
||||
Databases string `form:"databases" json:"databases"`
|
||||
Requirepass string `form:"requirepass" json:"requirepass"`
|
||||
Timeout string `form:"timeout" json:"timeout"`
|
||||
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive"`
|
||||
// 内存
|
||||
Maxmemory string `form:"maxmemory" json:"maxmemory"`
|
||||
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy"`
|
||||
// 持久化
|
||||
Appendonly string `form:"appendonly" json:"appendonly"`
|
||||
Appendfsync string `form:"appendfsync" json:"appendfsync"`
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+296
-8
@@ -1,8 +1,15 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/pkg/api"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
@@ -18,19 +25,300 @@ type App struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// AppCustom 自定义编译参数(前置脚本在 configure 前执行,参数追加到 configure 末尾)
|
||||
type AppCustom struct {
|
||||
PreScript string `json:"pre_script"`
|
||||
Args string `json:"args"`
|
||||
}
|
||||
|
||||
// 支持自定义编译参数的应用与环境类型(源码编译类)
|
||||
var (
|
||||
customCompileApps = []string{"apache", "memcached", "nginx", "openresty", "pureftpd", "s3fs"}
|
||||
customCompileEnvTypes = []string{"php", "python"}
|
||||
)
|
||||
|
||||
// CustomCompileApp 判断应用是否支持自定义编译参数
|
||||
func CustomCompileApp(slug string) bool {
|
||||
return slices.Contains(customCompileApps, slug)
|
||||
}
|
||||
|
||||
// CustomCompileEnv 判断环境类型是否支持自定义编译参数
|
||||
func CustomCompileEnv(typ string) bool {
|
||||
return slices.Contains(customCompileEnvTypes, typ)
|
||||
}
|
||||
|
||||
type AppRepo interface {
|
||||
Categories() []types.LV
|
||||
All() api.Apps
|
||||
Get(slug string) (*api.App, error)
|
||||
UpdateExist(slug string) bool
|
||||
Installed() ([]*App, error)
|
||||
GetInstalled(slug string) (*App, error)
|
||||
GetInstalledAll(query string, cond ...string) ([]*App, error)
|
||||
GetHomeShow() ([]map[string]string, error)
|
||||
ListHomeShow() ([]*App, error)
|
||||
IsInstalled(query string, cond ...any) (bool, error)
|
||||
Install(channel, slug string) error
|
||||
UnInstall(slug string) error
|
||||
Update(slug string) error
|
||||
UpdateShow(slug string, show bool) error
|
||||
UpdateOrder(slugs []string) error
|
||||
DownloadCallback(slug string)
|
||||
CheckPanelVersion() error
|
||||
ResolveScript(item *api.App, matchChannel, action, execVersion string) (string, error)
|
||||
ExecScript(script string) error
|
||||
PreCheck(item *api.App, catalog api.Apps) error
|
||||
GetCustom(slug string) (*AppCustom, error)
|
||||
SaveCustom(slug string, custom *AppCustom) error
|
||||
}
|
||||
|
||||
type AppUsecase struct {
|
||||
repo AppRepo
|
||||
cache CacheRepo
|
||||
task TaskRepo
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewAppUsecase(t *gotext.Locale, appRepo AppRepo, cacheRepo CacheRepo, taskRepo TaskRepo) *AppUsecase {
|
||||
return &AppUsecase{
|
||||
repo: appRepo,
|
||||
cache: cacheRepo,
|
||||
task: taskRepo,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) Categories() []types.LV {
|
||||
cached, err := uc.cache.Get(CacheKeyCategories)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var categories api.Categories
|
||||
if err = json.Unmarshal([]byte(cached), &categories); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
slices.SortFunc(categories, func(a, b *api.Category) int {
|
||||
return a.Order - b.Order
|
||||
})
|
||||
|
||||
return lo.Map(categories, func(item *api.Category, _ int) types.LV {
|
||||
return types.LV{Label: item.Name, Value: item.Slug}
|
||||
})
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) All() api.Apps {
|
||||
cached, err := uc.cache.Get(CacheKeyApps)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var apps api.Apps
|
||||
if err = json.Unmarshal([]byte(cached), &apps); err != nil {
|
||||
return nil
|
||||
}
|
||||
return apps
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) Get(slug string) (*api.App, error) {
|
||||
for item := range slices.Values(uc.All()) {
|
||||
if item.Slug == slug {
|
||||
return item, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New(uc.t.Get("app %s not found", slug))
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) UpdateExist(slug string) bool {
|
||||
item, err := uc.Get(slug)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
installed, err := uc.repo.GetInstalled(slug)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for channel := range slices.Values(item.Channels) {
|
||||
if channel.Slug == installed.Channel {
|
||||
if channel.Version != installed.Version {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) Installed() ([]*App, error) {
|
||||
return uc.repo.Installed()
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) GetInstalled(slug string) (*App, error) {
|
||||
return uc.repo.GetInstalled(slug)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) GetInstalledAll(query string, cond ...string) ([]*App, error) {
|
||||
return uc.repo.GetInstalledAll(query, cond...)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) GetHomeShow() ([]map[string]string, error) {
|
||||
apps, err := uc.repo.ListHomeShow()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 目录只加载一次
|
||||
catalog := lo.KeyBy(uc.All(), func(item *api.App) string { return item.Slug })
|
||||
|
||||
filtered := make([]map[string]string, 0)
|
||||
for item := range slices.Values(apps) {
|
||||
loaded, ok := catalog[item.Slug]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, map[string]string{
|
||||
"name": loaded.Name,
|
||||
"description": loaded.Description,
|
||||
"slug": loaded.Slug,
|
||||
"version": item.Version,
|
||||
})
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) IsInstalled(query string, cond ...any) (bool, error) {
|
||||
return uc.repo.IsInstalled(query, cond...)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) Install(channel, slug string) error {
|
||||
item, err := uc.Get(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 恢复原编排:非法面板版本应在存在性检查前报错
|
||||
if err = uc.repo.CheckPanelVersion(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if installed, _ := uc.repo.IsInstalled(slug); installed {
|
||||
return errors.New(uc.t.Get("app %s already installed", slug))
|
||||
}
|
||||
|
||||
script, err := uc.repo.ResolveScript(item, channel, "install", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.PreCheck(item, uc.All()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.repo.DownloadCallback(slug)
|
||||
|
||||
if app.IsCli {
|
||||
return uc.repo.ExecScript(script)
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
task.Key = "app:" + slug
|
||||
task.Name = uc.t.Get("Install app %s", item.Name)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = script
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) UnInstall(slug string) error {
|
||||
item, err := uc.Get(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 恢复原编排:非法面板版本应在存在性检查前报错
|
||||
if err = uc.repo.CheckPanelVersion(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if installed, _ := uc.repo.IsInstalled(slug); !installed {
|
||||
return errors.New(uc.t.Get("app %s not installed", item.Name))
|
||||
}
|
||||
installed, err := uc.repo.GetInstalled(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
script, err := uc.repo.ResolveScript(item, installed.Channel, "uninstall", installed.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.PreCheck(item, uc.All()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
return uc.repo.ExecScript(script)
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
task.Key = "app:" + slug
|
||||
task.Name = uc.t.Get("Uninstall app %s", item.Name)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = script
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) Update(slug string) error {
|
||||
item, err := uc.Get(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 恢复原编排:非法面板版本应在存在性检查前报错
|
||||
if err = uc.repo.CheckPanelVersion(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if installed, _ := uc.repo.IsInstalled(slug); !installed {
|
||||
return errors.New(uc.t.Get("app %s not installed", item.Name))
|
||||
}
|
||||
installed, err := uc.repo.GetInstalled(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
script, err := uc.repo.ResolveScript(item, installed.Channel, "update", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = uc.repo.PreCheck(item, uc.All()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.repo.DownloadCallback(slug)
|
||||
|
||||
if app.IsCli {
|
||||
return uc.repo.ExecScript(script)
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
task.Key = "app:" + slug
|
||||
task.Name = uc.t.Get("Update app %s", item.Name)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = script
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) UpdateShow(slug string, show bool) error {
|
||||
return uc.repo.UpdateShow(slug, show)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) GetCustom(slug string) (*AppCustom, error) {
|
||||
return uc.repo.GetCustom(slug)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) SaveCustom(slug string, custom *AppCustom) error {
|
||||
return uc.repo.SaveCustom(slug, custom)
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) UpdateOrder(slugs []string) error {
|
||||
return uc.repo.UpdateOrder(slugs)
|
||||
}
|
||||
|
||||
+115
-10
@@ -2,6 +2,10 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
@@ -9,25 +13,126 @@ import (
|
||||
type BackupType string
|
||||
|
||||
const (
|
||||
BackupTypePath BackupType = "path"
|
||||
BackupTypeWebsite BackupType = "website"
|
||||
BackupTypeMySQL BackupType = "mysql"
|
||||
BackupTypePostgres BackupType = "postgresql"
|
||||
BackupTypeRedis BackupType = "redis"
|
||||
BackupTypePanel BackupType = "panel"
|
||||
BackupTypePath BackupType = "path"
|
||||
BackupTypeWebsite BackupType = "website"
|
||||
BackupTypeMySQL BackupType = "mysql"
|
||||
BackupTypePostgres BackupType = "postgresql"
|
||||
BackupTypeClickHouse BackupType = "clickhouse"
|
||||
BackupTypeRedis BackupType = "redis"
|
||||
BackupTypeValkey BackupType = "valkey"
|
||||
BackupTypePanel BackupType = "panel"
|
||||
)
|
||||
|
||||
type BackupRepo interface {
|
||||
List(typ BackupType) ([]*types.BackupFile, error)
|
||||
GetStorage(id uint) (*BackupStorage, error)
|
||||
Create(ctx context.Context, typ BackupType, target string, account uint) error
|
||||
CreatePanel() error
|
||||
Delete(ctx context.Context, typ BackupType, name string) error
|
||||
Restore(ctx context.Context, typ BackupType, backup, target string) error
|
||||
Delete(typ BackupType, name string) error
|
||||
Restore(typ BackupType, backup, target string) error
|
||||
ClearExpired(path, prefix string, save uint) error
|
||||
ClearStorageExpired(account uint, typ BackupType, prefix string, save uint) error
|
||||
ClearStorageExpired(account uint, dir, prefix string, save uint) error
|
||||
CutoffLog(path, target string) (string, error)
|
||||
CutoffUpload(account uint, typ BackupType, name string, files []string) error
|
||||
GetDefaultPath(typ BackupType) string
|
||||
FixPanel() error
|
||||
UpdatePanel(version, url, checksum string) error
|
||||
UpdatePanel(version, url, checksum string, progress func(string)) error
|
||||
}
|
||||
|
||||
type BackupUsecase struct {
|
||||
repo BackupRepo
|
||||
log *slog.Logger
|
||||
notify *NotifyUsecase
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewBackupUsecase(notifyUsecase *NotifyUsecase, t *gotext.Locale, log *slog.Logger, backupRepo BackupRepo) *BackupUsecase {
|
||||
return &BackupUsecase{
|
||||
repo: backupRepo,
|
||||
log: log,
|
||||
notify: notifyUsecase,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) List(typ BackupType) ([]*types.BackupFile, error) {
|
||||
return uc.repo.List(typ)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) Create(ctx context.Context, typ BackupType, target string, account uint) error {
|
||||
err := uc.repo.Create(ctx, typ, target, account)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 定时备份由 CLI 执行,命令返回即退出,异步通知来不及发出,必须同步发送
|
||||
if sendErr := uc.notify.SendEventSync(ctx, NotifyEventBackup, uc.t.Get("[AcePanel] Backup Failed"), NotifyBody(uc.t.Get("backup task failed"), [][2]string{
|
||||
{uc.t.Get("Type"), string(typ)},
|
||||
{uc.t.Get("Target"), target},
|
||||
{uc.t.Get("Error"), err.Error()},
|
||||
{uc.t.Get("Time"), time.Now().Format(time.DateTime)},
|
||||
})); sendErr != nil {
|
||||
uc.log.Warn("failed to send backup failure notification", slog.Any("err", sendErr))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) CreatePanel() error {
|
||||
return uc.repo.CreatePanel()
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) Delete(ctx context.Context, typ BackupType, name string) error {
|
||||
if err := uc.repo.Delete(typ, name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("backup deleted", slog.String("type", OperationTypeBackup), slog.Uint64("operator_id", operatorID(ctx)), slog.String("backup_type", string(typ)), slog.String("name", name))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) Restore(ctx context.Context, typ BackupType, backup, target string) error {
|
||||
if err := uc.repo.Restore(typ, backup, target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("backup restored",
|
||||
slog.String("type", OperationTypeBackup),
|
||||
slog.Uint64("operator_id", operatorID(ctx)),
|
||||
slog.String("backup_type", string(typ)),
|
||||
slog.String("target", target),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) ClearExpired(path, prefix string, save uint) error {
|
||||
return uc.repo.ClearExpired(path, prefix, save)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) ClearStorageExpired(account uint, dir, prefix string, save uint) error {
|
||||
return uc.repo.ClearStorageExpired(account, dir, prefix, save)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) CutoffLog(path, target string) (string, error) {
|
||||
return uc.repo.CutoffLog(path, target)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) CutoffUpload(account uint, typ BackupType, name string, files []string) error {
|
||||
return uc.repo.CutoffUpload(account, typ, name, files)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) GetDefaultPath(typ BackupType) string {
|
||||
return uc.repo.GetDefaultPath(typ)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) FixPanel() error {
|
||||
return uc.repo.FixPanel()
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) UpdatePanel(version, url, checksum string, progress func(string)) error {
|
||||
return uc.repo.UpdatePanel(version, url, checksum, progress)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/utils/crypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -129,9 +131,110 @@ func (r *BackupStorage) AfterFind(tx *gorm.DB) error {
|
||||
}
|
||||
|
||||
type BackupAccountRepo interface {
|
||||
List(page, limit uint) ([]*BackupStorage, int64, error)
|
||||
Get(id uint) (*BackupStorage, error)
|
||||
Create(ctx context.Context, req *request.BackupStorageCreate) (*BackupStorage, error)
|
||||
Update(ctx context.Context, req *request.BackupStorageUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
ListPaged(page, limit uint) ([]*BackupStorage, int64, error)
|
||||
GetByID(id uint) (*BackupStorage, error)
|
||||
Create(account *BackupStorage) error
|
||||
Update(account *BackupStorage) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
type BackupAccountUsecase struct {
|
||||
repo BackupAccountRepo
|
||||
setting SettingRepo
|
||||
t *gotext.Locale
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewBackupAccountUsecase(t *gotext.Locale, log *slog.Logger, backupAccountRepo BackupAccountRepo, settingRepo SettingRepo) *BackupAccountUsecase {
|
||||
return &BackupAccountUsecase{
|
||||
repo: backupAccountRepo,
|
||||
setting: settingRepo,
|
||||
t: t,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *BackupAccountUsecase) List(page, limit uint) ([]*BackupStorage, int64, error) {
|
||||
// 本地存储
|
||||
localStorage, err := uc.Get(0)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
dbAccounts, total, err := uc.repo.ListPaged(page, limit)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
accounts := make([]*BackupStorage, 0, len(dbAccounts)+1)
|
||||
if page == 1 {
|
||||
accounts = append(accounts, localStorage)
|
||||
}
|
||||
accounts = append(accounts, dbAccounts...)
|
||||
|
||||
return accounts, total + 1, nil
|
||||
}
|
||||
|
||||
func (uc *BackupAccountUsecase) Get(id uint) (*BackupStorage, error) {
|
||||
if id == 0 {
|
||||
path, err := uc.setting.Get(SettingKeyBackupPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BackupStorage{
|
||||
ID: 0,
|
||||
Type: BackupStorageTypeLocal,
|
||||
Name: uc.t.Get("Local Storage"),
|
||||
Info: types.BackupStorageInfo{
|
||||
Path: path,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return uc.repo.GetByID(id)
|
||||
}
|
||||
|
||||
func (uc *BackupAccountUsecase) Create(ctx context.Context, req *request.BackupStorageCreate) (*BackupStorage, error) {
|
||||
account := &BackupStorage{
|
||||
Type: BackupStorageType(req.Type),
|
||||
Name: req.Name,
|
||||
Info: req.Info,
|
||||
}
|
||||
|
||||
if err := uc.repo.Create(account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uc.log.Info("backup storage created", slog.String("type", OperationTypeBackup), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(account.ID)), slog.String("account_type", req.Type), slog.String("name", req.Name))
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func (uc *BackupAccountUsecase) Update(ctx context.Context, req *request.BackupStorageUpdate) error {
|
||||
account, err := uc.Get(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
account.Type = BackupStorageType(req.Type)
|
||||
account.Name = req.Name
|
||||
account.Info = req.Info
|
||||
|
||||
if err = uc.repo.Update(account); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.log.Info("backup storage updated", slog.String("type", OperationTypeBackup), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(req.ID)), slog.String("account_type", req.Type))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *BackupAccountUsecase) Delete(ctx context.Context, id uint) error {
|
||||
if err := uc.repo.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uc.log.Info("backup storage deleted", slog.String("type", OperationTypeBackup), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package biz
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewAlertUsecase, NewAppUsecase, NewBackupUsecase, NewBackupAccountUsecase,
|
||||
NewCacheUsecase, NewCertUsecase, NewCertAccountUsecase,
|
||||
NewCertDNSUsecase, NewContainerUsecase, NewContainerComposeUsecase,
|
||||
NewContainerImageUsecase, NewContainerNetworkUsecase, NewContainerVolumeUsecase,
|
||||
NewCronUsecase, NewDatabaseUsecase, NewDatabaseRedisUsecase,
|
||||
NewDatabaseElasticsearchUsecase, NewDatabaseServerUsecase, NewDatabaseUserUsecase,
|
||||
NewEnvironmentUsecase, NewFileShareUsecase, NewLogUsecase, NewMonitorUsecase,
|
||||
NewNotifyUsecase, NewProjectUsecase, NewSafeUsecase, NewScanEventUsecase,
|
||||
NewSettingUsecase, NewSSHUsecase, NewTamperUsecase, NewTaskUsecase,
|
||||
NewTemplateUsecase, NewUserUsecase, NewUserPasskeyUsecase,
|
||||
NewUserTokenUsecase, NewWebHookUsecase, NewWebsiteUsecase,
|
||||
NewWebsiteStatUsecase, NewToolboxMigrationUsecase,
|
||||
)
|
||||
+89
-5
@@ -1,6 +1,13 @@
|
||||
package biz
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/api"
|
||||
"github.com/acepanel/panel/v3/pkg/apploader"
|
||||
)
|
||||
|
||||
type CacheKey string
|
||||
|
||||
@@ -21,8 +28,85 @@ type Cache struct {
|
||||
type CacheRepo interface {
|
||||
Get(key CacheKey, defaultValue ...string) (string, error)
|
||||
Set(key CacheKey, value string) error
|
||||
UpdateCategories() error
|
||||
UpdateApps() error
|
||||
UpdateEnvironments() error
|
||||
UpdateTemplates() error
|
||||
FetchCategories() (*api.Categories, error)
|
||||
FetchApps() (*api.Apps, error)
|
||||
FetchEnvironments() (*api.Environments, error)
|
||||
FetchTemplates() (*api.Templates, error)
|
||||
}
|
||||
|
||||
type CacheUsecase struct {
|
||||
repo CacheRepo
|
||||
}
|
||||
|
||||
func NewCacheUsecase(repo CacheRepo) *CacheUsecase {
|
||||
return &CacheUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) Get(key CacheKey, defaultValue ...string) (string, error) {
|
||||
return uc.repo.Get(key, defaultValue...)
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) Set(key CacheKey, value string) error {
|
||||
return uc.repo.Set(key, value)
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) UpdateCategories() error {
|
||||
categories, err := uc.repo.FetchCategories()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(categories)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.repo.Set(CacheKeyCategories, string(encoded))
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) UpdateApps() error {
|
||||
remote, err := uc.repo.FetchApps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 去除本地不存在的应用
|
||||
*remote = slices.Clip(slices.DeleteFunc(*remote, func(item *api.App) bool {
|
||||
return !slices.Contains(apploader.SupportedSlugs(), item.Slug)
|
||||
}))
|
||||
|
||||
encoded, err := json.Marshal(remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.repo.Set(CacheKeyApps, string(encoded))
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) UpdateEnvironments() error {
|
||||
environments, err := uc.repo.FetchEnvironments()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(environments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.repo.Set(CacheKeyEnvironment, string(encoded))
|
||||
}
|
||||
|
||||
func (uc *CacheUsecase) UpdateTemplates() error {
|
||||
templates, err := uc.repo.FetchTemplates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(templates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return uc.repo.Set(CacheKeyTemplates, string(encoded))
|
||||
}
|
||||
|
||||
+420
-17
@@ -2,19 +2,27 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
mholtacme "github.com/mholt/acmez/v3/acme"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/http/request"
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/acme"
|
||||
pkgcert "github.com/acepanel/panel/v3/pkg/cert"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type Cert struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
AccountID uint `gorm:"not null;default:0" json:"account_id"` // 关联的 ACME 账户 ID
|
||||
WebsiteID uint `gorm:"not null;default:0" json:"website_id"` // 关联的网站 ID
|
||||
DNSID uint `gorm:"not null;default:0" json:"dns_id"` // 关联的 DNS ID
|
||||
Type string `gorm:"not null;default:''" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096)
|
||||
Domains []string `gorm:"not null;default:'[]';serializer:json" json:"domains"` // 域名
|
||||
@@ -28,25 +36,420 @@ type Cert struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
Website *Website `gorm:"foreignKey:WebsiteID" json:"website"`
|
||||
Account *CertAccount `gorm:"foreignKey:AccountID" json:"account"`
|
||||
DNS *CertDNS `gorm:"foreignKey:DNSID" json:"dns"`
|
||||
Websites []*Website `gorm:"foreignKey:CertID" json:"websites"` // 部署的网站
|
||||
Account *CertAccount `gorm:"foreignKey:AccountID" json:"account"`
|
||||
DNS *CertDNS `gorm:"foreignKey:DNSID" json:"dns"`
|
||||
}
|
||||
|
||||
// WebsiteIDs 取证书关联的网站 ID 列表
|
||||
func (r *Cert) WebsiteIDs() []uint {
|
||||
return lo.Map(r.Websites, func(website *Website, _ int) uint {
|
||||
return website.ID
|
||||
})
|
||||
}
|
||||
|
||||
type CertRepo interface {
|
||||
List(page, limit uint) ([]*types.CertList, int64, error)
|
||||
Get(id uint) (*Cert, error)
|
||||
GetByWebsite(WebsiteID uint) (*Cert, error)
|
||||
Upload(ctx context.Context, req *request.CertUpload) (*Cert, error)
|
||||
Create(ctx context.Context, req *request.CertCreate) (*Cert, error)
|
||||
Update(ctx context.Context, req *request.CertUpdate) error
|
||||
Delete(ctx context.Context, id uint) error
|
||||
ObtainAuto(id uint) (*acme.Certificate, error)
|
||||
ObtainAutoWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error)
|
||||
ObtainPanel(account *CertAccount, ips []string) ([]byte, []byte, error)
|
||||
ObtainSelfSigned(id uint) error
|
||||
Renew(id uint) (*acme.Certificate, error)
|
||||
RenewWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error)
|
||||
RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error)
|
||||
Deploy(ID, WebsiteID uint, enableHTTPS bool) error
|
||||
Create(req *request.CertCreate) (*Cert, error)
|
||||
CreateUploaded(cert *Cert) error
|
||||
Update(req *request.CertUpdate) error
|
||||
Delete(id uint) error
|
||||
Save(cert *Cert) error
|
||||
GetClient(cert *Cert) (*acme.Client, error)
|
||||
GenerateSelfSigned(domains []string) ([]byte, []byte, error)
|
||||
RunScript(cert *Cert) error
|
||||
ObtainPanel(account *CertAccount, names []string, webServer string) ([]byte, []byte, error)
|
||||
LoadWebsites(websiteIDs []uint) ([]*Website, error)
|
||||
BindWebsites(certID uint, websiteIDs []uint) error
|
||||
HTTPConfs(cert *Cert, webServer string) (map[string]string, []string)
|
||||
WriteCertFiles(cert *Cert, certPath, keyPath string) error
|
||||
EnableWebsiteSSL(website *Website, certPath, keyPath, webServer string, tlsVersions []string, listenIPv6 bool) error
|
||||
ReloadWebserver(webServer string) error
|
||||
}
|
||||
|
||||
type CertUsecase struct {
|
||||
repo CertRepo
|
||||
setting SettingRepo
|
||||
t *gotext.Locale
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewCertUsecase(t *gotext.Locale, log *slog.Logger, certRepo CertRepo, settingRepo SettingRepo) *CertUsecase {
|
||||
return &CertUsecase{
|
||||
repo: certRepo,
|
||||
setting: settingRepo,
|
||||
t: t,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) List(page, limit uint) ([]*types.CertList, int64, error) {
|
||||
return uc.repo.List(page, limit)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Get(id uint) (*Cert, error) {
|
||||
return uc.repo.Get(id)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) GetByWebsite(websiteID uint) (*Cert, error) {
|
||||
return uc.repo.GetByWebsite(websiteID)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Upload(ctx context.Context, req *request.CertUpload) (*Cert, error) {
|
||||
info, err := pkgcert.ParseCert([]byte(req.Cert))
|
||||
if err != nil {
|
||||
return nil, errors.New(uc.t.Get("failed to parse certificate: %v", err))
|
||||
}
|
||||
if _, err = pkgcert.ParseKey([]byte(req.Key)); err != nil {
|
||||
return nil, errors.New(uc.t.Get("failed to parse private key: %v", err))
|
||||
}
|
||||
|
||||
// 合并 DNSNames 和 IPAddresses
|
||||
domains := info.DNSNames
|
||||
for _, ip := range info.IPAddresses {
|
||||
domains = append(domains, ip.String())
|
||||
}
|
||||
|
||||
cert := &Cert{
|
||||
Type: "upload",
|
||||
Domains: domains,
|
||||
Cert: req.Cert,
|
||||
Key: req.Key,
|
||||
}
|
||||
if err = uc.repo.CreateUploaded(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert uploaded", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(cert.ID)))
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Create(ctx context.Context, req *request.CertCreate) (*Cert, error) {
|
||||
cert, err := uc.repo.Create(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert created", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(cert.ID)), slog.String("cert_type", req.Type))
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Update(ctx context.Context, req *request.CertUpdate) error {
|
||||
info, err := pkgcert.ParseCert([]byte(req.Cert))
|
||||
if err == nil && req.Type == "upload" {
|
||||
// 合并 DNSNames 和 IPAddresses
|
||||
req.Domains = info.DNSNames
|
||||
for _, ip := range info.IPAddresses {
|
||||
req.Domains = append(req.Domains, ip.String())
|
||||
}
|
||||
}
|
||||
if req.Type == "upload" && req.AutoRenewal {
|
||||
return errors.New(uc.t.Get("upload certificate cannot be set to auto renewal"))
|
||||
}
|
||||
|
||||
if err = uc.repo.Update(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert updated", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(req.ID)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Delete(ctx context.Context, id uint) error {
|
||||
if err := uc.repo.Delete(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
uc.log.Info("cert deleted", slog.String("type", OperationTypeCert), slog.Uint64("operator_id", operatorID(ctx)), slog.Uint64("id", uint64(id)))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
return uc.ObtainAutoWithProgressCallback(context.Background(), id, nil)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) ObtainAutoWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error) {
|
||||
report := func(msg string) {
|
||||
if progressCallback != nil {
|
||||
progressCallback(msg)
|
||||
}
|
||||
}
|
||||
|
||||
report(uc.t.Get("initializing ACME client"))
|
||||
cert, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := uc.repo.GetClient(cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
webServer, _ := uc.setting.Get(SettingKeyWebserver)
|
||||
|
||||
if cert.DNS != nil {
|
||||
client.UseDns(cert.DNS.Type, cert.DNS.Data, acme.DnsOption{
|
||||
Alias: cert.Alias,
|
||||
DnsServer: cert.DNS.Data.DnsServer,
|
||||
SkipVerify: cert.DNS.Data.SkipVerify,
|
||||
ProgressCallback: progressCallback,
|
||||
})
|
||||
} else {
|
||||
if len(cert.Websites) == 0 {
|
||||
return nil, errors.New(uc.t.Get("this certificate is not associated with a website and cannot be obtained. You can try to obtain it manually"))
|
||||
}
|
||||
hasWildcard := slices.ContainsFunc(cert.Domains, func(d string) bool {
|
||||
return strings.Contains(d, "*")
|
||||
})
|
||||
if hasWildcard {
|
||||
return nil, errors.New(uc.t.Get("wildcard domains cannot use HTTP verification"))
|
||||
}
|
||||
confs, fallback := uc.repo.HTTPConfs(cert, webServer)
|
||||
client.UseHTTP(confs, fallback, webServer)
|
||||
}
|
||||
|
||||
report(uc.t.Get("issuing certificate, domains: %s", strings.Join(cert.Domains, ", ")))
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
defer cancel()
|
||||
ssl, err := client.ObtainCertificate(ctx, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
report(uc.t.Get("obtaining and saving certificate"))
|
||||
cert.RenewalInfo = *ssl.RenewalInfo
|
||||
cert.CertURL = ssl.URL
|
||||
cert.Cert = string(ssl.ChainPEM)
|
||||
cert.Key = string(ssl.PrivateKey)
|
||||
if err = uc.repo.Save(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(cert.Websites) > 0 {
|
||||
report(uc.t.Get("deploying certificate to website"))
|
||||
return &ssl, uc.Deploy(cert.ID, cert.WebsiteIDs(), false)
|
||||
}
|
||||
|
||||
if err = uc.repo.RunScript(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ssl, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) ObtainPanel(account *CertAccount, domains []string) ([]byte, []byte, error) {
|
||||
names := domains
|
||||
if len(names) == 0 {
|
||||
var err error
|
||||
names, err = uc.setting.GetSlice(SettingKeyPublicIPs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return nil, nil, errors.New(uc.t.Get("Please set the panel IP in settings first for ACME certificate generation"))
|
||||
}
|
||||
}
|
||||
|
||||
webServer, _ := uc.setting.Get(SettingKeyWebserver)
|
||||
return uc.repo.ObtainPanel(account, names, webServer)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) ObtainSelfSigned(id uint) error {
|
||||
cert, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
crt, key, err := uc.repo.GenerateSelfSigned(cert.Domains)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert.Cert = string(crt)
|
||||
cert.Key = string(key)
|
||||
if err = uc.repo.Save(cert); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(cert.Websites) > 0 {
|
||||
return uc.Deploy(cert.ID, cert.WebsiteIDs(), false)
|
||||
}
|
||||
|
||||
if err = uc.repo.RunScript(cert); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Renew(id uint) (*acme.Certificate, error) {
|
||||
return uc.RenewWithProgressCallback(context.Background(), id, nil)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) RenewWithProgressCallback(ctx context.Context, id uint, progressCallback func(string)) (*acme.Certificate, error) {
|
||||
report := func(msg string) {
|
||||
if progressCallback != nil {
|
||||
progressCallback(msg)
|
||||
}
|
||||
}
|
||||
|
||||
report(uc.t.Get("preparing renewal"))
|
||||
cert, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := uc.repo.GetClient(cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cert.CertURL == "" {
|
||||
return nil, errors.New(uc.t.Get("this certificate has not been obtained successfully and cannot be renewed"))
|
||||
}
|
||||
|
||||
webServer, _ := uc.setting.Get(SettingKeyWebserver)
|
||||
|
||||
if cert.DNS != nil {
|
||||
client.UseDns(cert.DNS.Type, cert.DNS.Data, acme.DnsOption{
|
||||
Alias: cert.Alias,
|
||||
DnsServer: cert.DNS.Data.DnsServer,
|
||||
SkipVerify: cert.DNS.Data.SkipVerify,
|
||||
ProgressCallback: progressCallback,
|
||||
})
|
||||
} else {
|
||||
if len(cert.Websites) == 0 {
|
||||
return nil, errors.New(uc.t.Get("this certificate is not associated with a website and cannot be obtained. You can try to obtain it manually"))
|
||||
}
|
||||
for _, domain := range cert.Domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
return nil, errors.New(uc.t.Get("wildcard domains cannot use HTTP verification"))
|
||||
}
|
||||
}
|
||||
confs, fallback := uc.repo.HTTPConfs(cert, webServer)
|
||||
client.UseHTTP(confs, fallback, webServer)
|
||||
}
|
||||
|
||||
report(uc.t.Get("renewing certificate, domains: %s", strings.Join(cert.Domains, ", ")))
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
||||
defer cancel()
|
||||
ssl, err := client.RenewCertificate(ctx, cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
// 续签失败,尝试重签
|
||||
report(uc.t.Get("renewal failed, attempting re-issuance"))
|
||||
ssl, err = client.ObtainCertificate(ctx, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
report(uc.t.Get("obtaining and saving certificate"))
|
||||
cert.RenewalInfo = *ssl.RenewalInfo
|
||||
cert.CertURL = ssl.URL
|
||||
cert.Cert = string(ssl.ChainPEM)
|
||||
cert.Key = string(ssl.PrivateKey)
|
||||
if err = uc.repo.Save(cert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(cert.Websites) > 0 {
|
||||
report(uc.t.Get("deploying certificate to website"))
|
||||
return &ssl, uc.Deploy(cert.ID, cert.WebsiteIDs(), false)
|
||||
}
|
||||
|
||||
return &ssl, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error) {
|
||||
cert, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
client, err := uc.repo.GetClient(cert)
|
||||
if err != nil {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
|
||||
crt, err := pkgcert.ParseCert([]byte(cert.Cert))
|
||||
if err != nil {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||
defer cancel()
|
||||
renewInfo, err := client.GetRenewalInfo(ctx, crt)
|
||||
if err != nil {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
|
||||
cert.RenewalInfo = renewInfo
|
||||
if err = uc.repo.Save(cert); err != nil {
|
||||
return mholtacme.RenewalInfo{}, err
|
||||
}
|
||||
|
||||
return renewInfo, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Deploy(id uint, websiteIDs []uint, enableHTTPS bool) error {
|
||||
cert, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cert.Cert == "" || cert.Key == "" {
|
||||
return errors.New(uc.t.Get("this certificate has not been obtained successfully and cannot be deployed"))
|
||||
}
|
||||
|
||||
websites, err := uc.repo.LoadWebsites(websiteIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 建立关联,使续签后能自动部署到这些网站
|
||||
if err = uc.repo.BindWebsites(cert.ID, websiteIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webServer, webServerErr := uc.setting.Get(SettingKeyWebserver)
|
||||
tlsVersions, _ := uc.setting.GetSlice(SettingKeyWebsiteTLSVersions)
|
||||
listenIPv6, err := uc.setting.GetBool(SettingKeyWebsiteListenIPv6, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, website := range websites {
|
||||
configDir := filepath.Join(app.Root, "sites", website.Name, "config")
|
||||
certPath := filepath.Join(configDir, "fullchain.pem")
|
||||
keyPath := filepath.Join(configDir, "private.key")
|
||||
if err = uc.repo.WriteCertFiles(cert, certPath, keyPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 开启 HTTPS
|
||||
if enableHTTPS && !website.SSL {
|
||||
if webServerErr != nil {
|
||||
return webServerErr
|
||||
}
|
||||
if err = uc.repo.EnableWebsiteSSL(website, certPath, keyPath, webServer, tlsVersions, listenIPv6); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uc.repo.ReloadWebserver(webServer)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Save(cert *Cert) error {
|
||||
return uc.repo.Save(cert)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user