mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-30 17:05:19 +08:00
Compare commits
1178 Commits
| 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 | |||
| db2bb0606d | |||
| a2d1c9c535 | |||
| f6ab265b6b | |||
| 4614fb1cd1 | |||
| 965525e68e | |||
| fdde61c8ae | |||
| 99bc8ddc8d | |||
| d4f85066a0 | |||
| 5263ec4a46 | |||
| 522d78569a | |||
| a42f9f63d0 | |||
| 7a7e8c4460 | |||
| 116fbdda89 | |||
| 87b728ca1b | |||
| 2320656d2b | |||
| 2e5d67e4b0 | |||
| 1c9ed32aaf | |||
| 9df648b7e9 | |||
| 1079065220 | |||
| 6f8c1b410f | |||
| 6e63f565fa | |||
| c24fa4580e | |||
| 2e6277848c | |||
| 35f9d16d81 | |||
| fcf0d49cba | |||
| 9c0f5d86d2 | |||
| 97161b8628 | |||
| 0b131b2920 | |||
| 37c2f851e5 | |||
| 9ec7b47cf6 | |||
| a4c59d29af | |||
| a5396fddbb | |||
| bc2584b7e5 | |||
| d839949684 | |||
| 3d81d1d0a3 | |||
| 0debf14dc3 | |||
| fc84ba4e86 | |||
| 7acd24c587 | |||
| 4275baabd0 | |||
| e6657c10e2 | |||
| 9e5cc546c1 | |||
| e799cb4031 | |||
| 12a6d6f39f | |||
| c33c03d225 | |||
| 715e58c122 | |||
| 9f8d684763 | |||
| 1d79f8c06e | |||
| 3fa4921b40 | |||
| 08bb081c66 | |||
| 4aa95c77f9 | |||
| 5ccaa58ccb | |||
| 1ac12b4ebd | |||
| 32067e6000 | |||
| 37fc3b29fa | |||
| e2ce8e70f1 | |||
| 60d4e31fac | |||
| 49b6c5af7e | |||
| 76f80a8e65 | |||
| dddb718fda | |||
| 046dc09f69 | |||
| 8d218e7e88 | |||
| 5fdae42eb7 | |||
| d01d14f35e | |||
| 5e251ac04a | |||
| b12590c1c2 | |||
| 4c6347d6c3 | |||
| f66b094ec6 | |||
| b0e04d2976 | |||
| 68b96be537 | |||
| 8ebe63ac3d | |||
| 4df9400fb2 | |||
| c40da3895d | |||
| ad409eb446 | |||
| 7c7d5b6ce1 | |||
| 5de7a8e745 | |||
| 2f9ccf23a0 | |||
| 3a7099b3d2 | |||
| e05202dafb | |||
| f8c99803ed | |||
| 3439783518 | |||
| c7929c2899 | |||
| 0d6a1b384d | |||
| 9216940d90 | |||
| d1371b2bb1 | |||
| e00013f801 | |||
| 130716216d | |||
| 9d22b5d1d0 | |||
| b952ebf5ad | |||
| 455f6c061d | |||
| 432b0662b0 | |||
| 0ae7327105 | |||
| 0870b100cc | |||
| 28b2eaa638 | |||
| bdcdfbf96e | |||
| 471db7ef69 | |||
| be67988272 | |||
| a56ef041c8 | |||
| 3b9c1757a0 | |||
| ed06f53791 | |||
| ff06049085 | |||
| b9b00a0117 | |||
| 61572b0a3c | |||
| 8fdbd19061 | |||
| c5e7084c8b | |||
| 71129d6446 | |||
| 615df5966b | |||
| d15ccba931 | |||
| d0ac218809 | |||
| f0cf40c5f0 | |||
| b100922bc7 | |||
| 69ff73a393 | |||
| 01585dcc3b | |||
| fe5daca28b | |||
| 3be7ec8a80 | |||
| 6426d2e7d2 | |||
| 663a303c2b | |||
| 1ce4628d87 | |||
| d45ddac1cc | |||
| 820b318c00 | |||
| 516f552181 | |||
| 228e2146ba | |||
| 007f7339f7 | |||
| 613ec00c90 | |||
| 8071ea1757 | |||
| 6da51ed0eb | |||
| aa1751f0bb | |||
| f15841f04a | |||
| ced266145b | |||
| 580a68202c | |||
| 81e99db86e | |||
| 499a570efa | |||
| 5258ef9d00 | |||
| c34203ae7b | |||
| d477d1c637 | |||
| 10c98c5bb5 | |||
| d8e8a305d3 | |||
| 2dba43fd20 | |||
| b5a65e6226 | |||
| 7295baa2d5 | |||
| 2c9b484218 | |||
| 0ac71f9f8a | |||
| 2217d2ba33 | |||
| f94345dd1c | |||
| 17eb9c6b63 | |||
| 0104418c10 | |||
| 24b0fb1a12 | |||
| e9c1a946e9 | |||
| 1afedfc4d8 | |||
| 971aaddc85 | |||
| 60052aac09 | |||
| 4a6fdc45d5 | |||
| 2be464777b | |||
| 7ea9f5a340 | |||
| 2142c4fd67 | |||
| f3e9d20990 | |||
| f1c17c4651 | |||
| 8ecdaf3c7a | |||
| bb2f9e02b5 | |||
| bbe0e9c4b4 | |||
| 9925a51cde | |||
| cb29a24751 | |||
| 5f62c6dd29 | |||
| 8647c10f3f | |||
| a04ac69392 | |||
| b011191d4c | |||
| e00c1eb1f7 | |||
| 597c374443 | |||
| 0a34d5f631 | |||
| 36b8500101 | |||
| 5044060661 | |||
| d96d83a5b7 | |||
| 99fe0b2719 | |||
| 06bd74be8c | |||
| fe0d709370 | |||
| e0f5704275 | |||
| 4eb80c8720 | |||
| d8213d65a0 | |||
| f00ceb9d10 | |||
| 8f96d53e09 | |||
| 22cfddf43b | |||
| 4479e5b9e9 | |||
| dbe4ea9909 | |||
| 9de68220ec | |||
| 83de33d1ab | |||
| d8515f9883 | |||
| 4ff7ce18cd | |||
| 71e259ba32 | |||
| 4873103904 | |||
| 55655d99cf | |||
| 830d4dca7a | |||
| 2a89e060c3 | |||
| 6900092723 | |||
| 8a79d90e78 | |||
| f5e5b687b8 | |||
| f5628cdf4a | |||
| 44f569b478 | |||
| 38ec8b63d2 | |||
| fad297f33d | |||
| 0c8a8cd382 | |||
| 3089a847a7 | |||
| 4b1839169a | |||
| 2aeec54189 | |||
| ce19d4ff14 | |||
| a67ec47c7e | |||
| 7fc0e0f4af | |||
| bfd4896470 | |||
| 12b8a4a03b | |||
| d4c853db01 | |||
| e947259b86 | |||
| c7cde220d4 | |||
| b4e7c924a9 | |||
| 6401f2ddd8 | |||
| 735258c212 | |||
| 358ee8d19c | |||
| dd5a9d3e72 | |||
| e92bc5309b | |||
| bb3cf83b59 | |||
| 82880f0ecc | |||
| 7d174655ee | |||
| ab27b6d1bc | |||
| 308e7c685c | |||
| 7de339b3f7 | |||
| 3445d2396e | |||
| 4130b8b314 | |||
| d216a72ac9 | |||
| 20c1cb842c | |||
| 7b94af32ea | |||
| 1d7f0a2f22 | |||
| 6c589d0350 | |||
| a0c6422a69 | |||
| c79e72a159 | |||
| 2791add4ef | |||
| f064eae300 | |||
| b86645bd58 | |||
| 04b4352abd | |||
| 42055275ca | |||
| 8facb097e1 | |||
| ec1e069b84 | |||
| f1d5122a13 | |||
| c5b4fb18d8 | |||
| c507eff0df | |||
| e29a98ecb2 | |||
| 2eb7aa207b | |||
| e5ed1bfd47 | |||
| 472aaada36 | |||
| 31ed50fad0 | |||
| 540e2232fd | |||
| 06ff48fd3f | |||
| f6ac168081 | |||
| 4004e61b55 | |||
| a62cff7a73 | |||
| 8bf30ee586 | |||
| 57340482cc | |||
| a233efa6db | |||
| 1a907ce8a8 | |||
| 64b671914c | |||
| cb2180fbdb | |||
| 67a4b2f8f1 | |||
| b39785fc34 | |||
| b9cb492847 | |||
| 0109eb9c52 | |||
| 959243690d | |||
| 6809969b25 | |||
| 87295af790 | |||
| 370e5d9a20 | |||
| b495017dab | |||
| 3eeff1575c | |||
| 0124481f6d | |||
| d569de3e67 | |||
| c64cf8ac18 | |||
| 6fc7b34882 | |||
| 016b06bfce | |||
| 08c3623526 | |||
| 632bdd0b1a | |||
| 2d8f014d20 | |||
| 6eac8176b3 | |||
| e77863d469 | |||
| 95c54fd17a | |||
| 527e49fde3 | |||
| ced99702a9 | |||
| 7190cc7947 | |||
| 8891420814 | |||
| 81ac5d1b8a | |||
| fe3405c4e2 | |||
| 90cd78dd1a | |||
| cc895d4df7 | |||
| da70928767 | |||
| cfb3ed75bd | |||
| 1e85a141f1 | |||
| 6418d88f22 | |||
| 013389af54 | |||
| f0121d7240 | |||
| 1133468ddf | |||
| 701d3e96db | |||
| 49268d3b08 | |||
| 95d38b6c02 | |||
| 9b4c6b2319 | |||
| 8c09c790a5 | |||
| b5232d47b1 | |||
| 1f28186581 | |||
| 537f1430de | |||
| f009b24ad9 | |||
| 93733b5692 | |||
| 2af6469a12 | |||
| 19a3a92216 | |||
| 16590d9b17 | |||
| eaae15c092 | |||
| b9d55f51b8 | |||
| 6c9e43e701 | |||
| 8952b7bdaf | |||
| b9b14f55c1 | |||
| 6e2dd1b91f | |||
| 4219c8d57e | |||
| dbaa68357e | |||
| 971b130061 | |||
| 003da4ce2d | |||
| 433db4cbd6 | |||
| 2b48fd5fe3 | |||
| 4c468e9aca | |||
| 7d368536d0 | |||
| 1403c65d37 | |||
| fa30728753 | |||
| aa2e54f3f1 | |||
| 20e3484767 | |||
| f80b954993 | |||
| 17efbab5dd | |||
| 7b1d647313 | |||
| 6cf84f148b | |||
| 8abae3bb4e | |||
| e95f904e87 | |||
| c67f2b7df0 | |||
| e6e358bd14 | |||
| bd9ba1c552 | |||
| 4b7cf9e981 | |||
| b75abff1c1 | |||
| e06917003d | |||
| 375cb73e34 | |||
| 1213c39fb2 | |||
| 397f84a921 | |||
| 96c03c4c5a | |||
| 5407c53619 | |||
| 37c155acb5 | |||
| 17adaa1c8c | |||
| 65e390a256 | |||
| 67642aa475 | |||
| da9d95284c | |||
| 9a1555c48a | |||
| d0cd178dfe | |||
| acacf5d9ce | |||
| e36bb8f429 | |||
| c731125534 | |||
| 1cf3218fde | |||
| b7be21c851 | |||
| 2f49d0baa6 | |||
| b541a56bf9 | |||
| bb722ccf3d | |||
| f19c0d9c00 | |||
| e9affee707 | |||
| ba6c09ed70 | |||
| 23e2df1ed9 | |||
| 0bafb5b8f5 | |||
| 98195d1838 | |||
| 2a143513d0 | |||
| 432fcdd0a0 | |||
| 6c703bf94d | |||
| b439973995 | |||
| 5c74b7eb71 | |||
| edc88484ac | |||
| 51e4cdb095 | |||
| e0c5f425cb | |||
| 2f2f17b1c7 | |||
| 2536dfe933 | |||
| e95c4623c5 | |||
| 3d46a284e6 | |||
| ed33e090ed | |||
| 0cab838499 | |||
| 4f7b6b1c38 | |||
| 230894d618 | |||
| ec8b3dbb10 | |||
| 4a899f1c0d | |||
| 49a5e36c30 | |||
| 47d945710b | |||
| 3ab58fc817 | |||
| 36a6617c68 | |||
| b8d2051e64 | |||
| da7c93308b | |||
| 017c0ed9b7 | |||
| 0930007d24 | |||
| 8285b8dee8 | |||
| c52d42a2de | |||
| 5d07932226 | |||
| 89ad4d83f3 | |||
| cce1102c90 | |||
| f67a3e3a6f | |||
| 7e09e20254 | |||
| 6fd6c958cf | |||
| 7fe0c3ce3c | |||
| 086d0eb601 | |||
| d7b7cf0b57 | |||
| 78a1a43f2f | |||
| f9c1510d8a | |||
| 0ea2117d96 | |||
| 2b055d6e88 | |||
| 227a7b19f3 | |||
| eda6d92d66 | |||
| c2441d2081 | |||
| 47123317a2 | |||
| 68125fb1e6 | |||
| acecdf7f48 | |||
| 021646727b | |||
| 4e6d46d1e9 | |||
| 1265ac5b34 | |||
| 94704deb1b | |||
| 932f48044b | |||
| 0c89d77221 | |||
| e0ccab8eb7 | |||
| 3aef5140e2 | |||
| 10ca20f3aa | |||
| 89a55ca518 | |||
| d743bef697 | |||
| 50fe450d64 | |||
| 74259f833c | |||
| ca8e36351e | |||
| d01a2374f5 | |||
| f4885c55f9 | |||
| 7e8de43776 | |||
| 4ded231b1c | |||
| 20a0080b1e | |||
| d2b30f211f | |||
| 83311409ae | |||
| 515ed686fb | |||
| 975c0d7321 | |||
| 0bdf1d6058 | |||
| c5d62a46ab | |||
| 2dd6f0d9f0 | |||
| de5d76bff6 | |||
| e3742e61d8 | |||
| 72c31a14b2 | |||
| 205d95a591 | |||
| 4df5038f91 | |||
| da023c5faa | |||
| e6c13330d8 | |||
| bda95465f8 | |||
| c3074249f0 | |||
| 6388bcfa37 | |||
| 281f35dfa1 | |||
| 5d189172ca | |||
| aa23066fe5 | |||
| c3313845a6 | |||
| 85f86f51db | |||
| 374996498f | |||
| a17b9d4936 | |||
| c226091c6e | |||
| 8902dade39 | |||
| 5f9696f214 | |||
| 7155e7c7cb | |||
| c56b4e00d9 | |||
| f7bf8b01cc | |||
| b7e1bd8824 | |||
| d9b6813060 | |||
| 10a816f7f7 | |||
| 68e4016538 | |||
| 338b9cc73b | |||
| b27ced4b92 | |||
| 36fb9e76fc | |||
| 5f7df1ef9a | |||
| 7da411b2f3 | |||
| 3c6c5aa31e | |||
| 5c05d0d81b | |||
| 0724a042ef | |||
| 261d87d6df | |||
| f043f65f95 | |||
| 0e56569dab | |||
| 8ccefa6bff | |||
| f3b8069710 | |||
| 8bbddef429 | |||
| 296930d36c | |||
| ae8e1458ea | |||
| 9625b28ee8 | |||
| bcff241c95 | |||
| b4df083a19 | |||
| ab756ed2bd | |||
| 5cd1af9c2a | |||
| dc8bd635d4 | |||
| 9cec03a69a | |||
| 5096339ab4 | |||
| d20b940d24 | |||
| 76abe1769b | |||
| 0d0c38a225 | |||
| 5ede0161c1 | |||
| 9378546cc4 | |||
| 33b0ee9d1e | |||
| 5994595198 | |||
| c84ab39f16 | |||
| 4760c89a06 | |||
| a10a06a544 | |||
| f1a9f5a4bc | |||
| 6466868e08 | |||
| fd65c267b3 | |||
| 5ee2409b87 | |||
| fca72a7e35 | |||
| 7b971c7920 | |||
| dbeb6b5fa9 | |||
| 402388c2d3 | |||
| 58c5a87262 | |||
| c4cd926622 | |||
| debc34f236 | |||
| 2c06ae9293 | |||
| bd7cb16c3a | |||
| 9b36edd1a9 | |||
| d5c0e03f16 | |||
| 50a5b42b85 | |||
| 57accdb5cf | |||
| f6cf1daae5 | |||
| 9cbf7b3f9a | |||
| 9700e432fd | |||
| efb427ff9c | |||
| 7ac05c5e29 | |||
| 181b262a61 | |||
| f7b63e2f6e | |||
| 5fc8fb1c1f | |||
| 805a7ea773 | |||
| c8bfe92c5e | |||
| 4b26e190f5 | |||
| 78be22b3dc | |||
| 1909865f04 | |||
| d7e8301148 | |||
| 54b1ddb809 | |||
| 30bf14c40c | |||
| 9ab01dc64b | |||
| 58ddd55b7c | |||
| 4f19a58bba | |||
| 15f096191e | |||
| 6f1b177642 | |||
| f0ae6278f1 | |||
| 2fdb23a568 | |||
| ba12def9f2 | |||
| c5667d8ce4 | |||
| 6b1789421f | |||
| cef8d7caa7 | |||
| 2d3250bc51 | |||
| 37f50eebd2 | |||
| 676bed3a41 | |||
| bee7da8a7e | |||
| 9cfa4738cf | |||
| 39d015b1cc | |||
| b9a9bf7904 | |||
| c35c26b237 | |||
| d25c419462 | |||
| 309f6b7ee2 | |||
| 7de7eabca3 | |||
| ce83fdd4db | |||
| 7ef6a5df0e | |||
| 624bac6ead | |||
| 2e4a5d3b43 | |||
| a6e4c17b83 | |||
| f3e31ab518 | |||
| 42facd50c3 | |||
| 706bee0e43 | |||
| d0c4a106fb | |||
| 571a63325b | |||
| 4ae47f03ef | |||
| 9e24a953c1 | |||
| 64ef88c28e | |||
| 43b92ceb4e | |||
| c2c3aa48f1 | |||
| be7996969a | |||
| eeb4aaee90 | |||
| fb7c56dc01 | |||
| 15abaa875d | |||
| 0ab2bbe92a | |||
| dd7d1ed490 | |||
| c9783118f5 | |||
| 626342d13b | |||
| 15287b5bce | |||
| f0f4b4f692 | |||
| ea0dccb6c1 | |||
| 6a73f6e333 | |||
| b5c9f3bfb9 | |||
| 6458957650 | |||
| bf8ca47c62 | |||
| e7fa6b6fb0 | |||
| 6f0b10653b | |||
| c9bc8aa193 | |||
| 6870388d07 | |||
| 8f9a32dfb0 | |||
| f87658f175 | |||
| 369ef07f19 | |||
| 6a3e0e547a | |||
| 3ba9b8433d | |||
| bd03013392 | |||
| bd746fe766 | |||
| eca6ae07be | |||
| ab6d6c6ba2 | |||
| 3547157850 | |||
| 01bda75ce5 | |||
| 2cfd5a26a9 | |||
| fd583ec208 | |||
| af2d856576 | |||
| d4a6b5e207 | |||
| ef6b83f425 | |||
| 73d3a5d86b | |||
| 8f9ff80fbf | |||
| 3ef55c5241 | |||
| 6db0035c43 | |||
| fd27f198b9 | |||
| e735d63c56 | |||
| c23d13437d | |||
| 4424d8ba18 | |||
| 74a1fd2258 | |||
| 84f5ea815b | |||
| 0fc6104d89 | |||
| 05ee59c51c | |||
| 68793e22dd | |||
| e27a95c72b | |||
| 755e430737 | |||
| 3169f3d036 | |||
| 23875834e8 | |||
| c094c919bc | |||
| 03de84439e | |||
| 975f306e71 | |||
| 0c546d67a4 | |||
| 8a5e1ed288 | |||
| 6a5ee1da8a | |||
| ce88078c58 | |||
| da41d23a36 | |||
| 2e3cb81f42 | |||
| b443dd6463 | |||
| dd15c47acf | |||
| 0410fed990 | |||
| 51535b979e | |||
| 36f2bdaf81 | |||
| 358e02f9a3 | |||
| 56c1dcbd44 | |||
| 8bb0d969c5 | |||
| 7cfd1f43c2 | |||
| 44bd8a0e63 | |||
| c39644c4f3 | |||
| 4904b7c96d | |||
| 021efa95ec | |||
| 9fce7323a6 | |||
| c4f6dd0be2 | |||
| 197781e722 | |||
| d87a9e3f02 | |||
| fba22ddae2 | |||
| 522c2386a1 | |||
| 7c6c5f213d | |||
| 667c0d4ce3 | |||
| 9f590fe3e9 | |||
| 0292d3d6c0 | |||
| 2c94cb62d8 | |||
| 276ada3ab2 | |||
| fb5b048e5e | |||
| 0e84a9bcbb | |||
| ea42eccbeb | |||
| f17616d3af | |||
| 90c753c6f4 | |||
| 50d0095a62 | |||
| 8fd18406b5 | |||
| 3c48269ee5 | |||
| 974ca958bc | |||
| f78bb65a66 | |||
| 283e4f0a79 |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -1,6 +1,164 @@
|
||||
## 角色定义
|
||||
|
||||
你是 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
|
||||
|
||||
@@ -13,11 +171,13 @@ AcePanel 是基于 Go 语言开发的新一代 Linux 服务器运维管理面板
|
||||
### 后端构建
|
||||
|
||||
构建主程序:
|
||||
|
||||
```bash
|
||||
go build -o ace ./cmd/ace
|
||||
```
|
||||
|
||||
构建 CLI 工具:
|
||||
|
||||
```bash
|
||||
go build -o cli ./cmd/cli
|
||||
```
|
||||
@@ -25,21 +185,25 @@ go build -o cli ./cmd/cli
|
||||
### 前端开发
|
||||
|
||||
进入前端目录:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
```
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
开发模式(带热重载):
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
构建生产版本:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
@@ -87,9 +251,9 @@ pnpm build
|
||||
|
||||
- **`internal/migration/`**: 数据库迁移
|
||||
|
||||
- **`internal/job/`**: 后台任务
|
||||
- **`internal/job/`**: 定时任务
|
||||
|
||||
- **`internal/queuejob/`**: 任务队列
|
||||
- **`internal/taskqueue/`**: 任务队列运行器(基于 DB 轮询,实现 `types.TaskRunner` 接口)
|
||||
|
||||
- **`pkg/`**: 工具函数和通用包
|
||||
- 包含各种独立的工具模块
|
||||
@@ -148,6 +312,7 @@ pnpm build
|
||||
### 助手函数(service 层)
|
||||
|
||||
在 service 层使用以下助手函数:
|
||||
|
||||
- `Success(w, data)`: 返回成功响应
|
||||
- `Error(w, statusCode, format, args...)`: 返回错误响应
|
||||
- `ErrorSystem(w, format, args...)`: 返回系统严重错误(500)
|
||||
@@ -170,6 +335,8 @@ pnpm build
|
||||
- 使用 `gofmt` 格式化代码
|
||||
- 复杂逻辑添加注释说明
|
||||
- 导出的函数和类型必须有注释
|
||||
- service 层以及 data 层方法对外返回的字符串需要使用 gotext 进行国际化处理
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
|
||||
## Wire 依赖注入
|
||||
|
||||
@@ -177,6 +344,7 @@ pnpm build
|
||||
|
||||
1. 在 `cmd/ace/wire.go` 或 `cmd/cli/wire.go` 中添加 provider
|
||||
2. 运行生成命令:
|
||||
|
||||
```bash
|
||||
go generate ./...
|
||||
```
|
||||
@@ -190,17 +358,31 @@ go generate ./...
|
||||
- 图标:@iconify/vue
|
||||
- 终端:xterm.js
|
||||
- 遵循项目已有的组件结构和编码风格
|
||||
- 禁止手动编辑国际化文件,项目使用外部 Crowdin 自动化管理
|
||||
|
||||
## 配置文件
|
||||
|
||||
开发时需要准备配置文件:
|
||||
|
||||
```bash
|
||||
cp config.example.yml config.yml
|
||||
```
|
||||
|
||||
前端开发配置:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
cp .env.production .env
|
||||
cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
```
|
||||
|
||||
## 工具使用
|
||||
|
||||
### 文档工具
|
||||
|
||||
1. **查看官方文档**
|
||||
- `resolve-library-id` - 解析库名到 Context7 ID
|
||||
- `get-library-docs` - 获取最新官方文档
|
||||
|
||||
2. **搜索真实代码**
|
||||
- `searchGitHub` - 搜索 GitHub 上的实际使用案例
|
||||
|
||||
+18
-17
@@ -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@v4
|
||||
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'
|
||||
@@ -27,11 +28,11 @@ jobs:
|
||||
working-directory: web
|
||||
run: |
|
||||
cp .env.production .env
|
||||
cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
cp config/proxy-config.example.ts config/proxy-config.ts
|
||||
pnpm run gettext:compile
|
||||
pnpm build
|
||||
- name: Upload frontend
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend
|
||||
path: web/dist/
|
||||
@@ -44,18 +45,18 @@ 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'
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Download frontend
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend
|
||||
path: pkg/embed/frontend
|
||||
@@ -75,17 +76,17 @@ jobs:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: |
|
||||
LDFLAGS="-s -w --extldflags '-static'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/internal/app.Version=${VERSION}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/internal/app.BuildTime=${BUILD_TIME}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/internal/app.CommitHash=${COMMIT_HASH}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/internal/app.GoVersion=${GO_VERSION}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/internal/app.BuildID=${BUILD_ID}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/internal/app.BuildUser=${BUILD_USER}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/internal/app.BuildHost=${BUILD_HOST}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/v3/internal/app.Version=${VERSION}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/v3/internal/app.BuildTime=${BUILD_TIME}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/v3/internal/app.CommitHash=${COMMIT_HASH}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/v3/internal/app.GoVersion=${GO_VERSION}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/v3/internal/app.BuildID=${BUILD_ID}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/v3/internal/app.BuildUser=${BUILD_USER}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/acepanel/panel/v3/internal/app.BuildHost=${BUILD_HOST}'"
|
||||
go build -trimpath -buildvcs=false -ldflags "${LDFLAGS}" -o ace-${{ matrix.goarch }} ./cmd/ace
|
||||
go build -trimpath -buildvcs=false -ldflags "${LDFLAGS}" -o cli-${{ matrix.goarch }} ./cmd/cli
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: backend-${{ matrix.goarch }}
|
||||
path: |
|
||||
|
||||
@@ -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@v4
|
||||
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'
|
||||
@@ -33,7 +34,7 @@ jobs:
|
||||
working-directory: web
|
||||
run: |
|
||||
cp .env.production .env
|
||||
cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
cp config/proxy-config.example.ts config/proxy-config.ts
|
||||
pnpm run gettext:compile
|
||||
pnpm build
|
||||
- name: Set environment variables
|
||||
@@ -41,7 +42,7 @@ jobs:
|
||||
echo "GOVERSION=$(go version | cut -d' ' -f3)" >> $GITHUB_ENV
|
||||
echo "HOSTNAME=$(hostname)" >> $GITHUB_ENV
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
name: Issue Auto Lock
|
||||
on:
|
||||
schedule:
|
||||
- cron: "*/5 * * * *"
|
||||
workflow_dispatch:
|
||||
issues:
|
||||
types: [ opened ]
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
jobs:
|
||||
issue-lock:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- name: Set GH_REPO
|
||||
run: echo "GH_REPO=${{ github.repository }}" >> $GITHUB_ENV
|
||||
- name: Auto Lock Issue
|
||||
uses: devhaozi/issue-auto-lock@v1
|
||||
with:
|
||||
gh_repo: ${{ github.repository }}
|
||||
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue_labels: "⭐ No Star"
|
||||
+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@v4
|
||||
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@v4
|
||||
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@v5
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
disable_search: true
|
||||
files: coverage.out
|
||||
|
||||
+4
-2
@@ -11,8 +11,8 @@ _cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
# 编译文件
|
||||
ace
|
||||
cli
|
||||
/ace
|
||||
/cli
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
@@ -35,6 +35,8 @@ cli
|
||||
*.log
|
||||
*.sqlite
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
config.yml
|
||||
|
||||
# 临时文件
|
||||
|
||||
+14
-14
@@ -17,13 +17,13 @@ builds:
|
||||
- -buildvcs=false
|
||||
ldflags:
|
||||
- -s -w --extldflags "-static"
|
||||
- -X 'github.com/acepanel/panel/internal/app.Version={{ .Version }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.BuildTime={{ .Now.Format "2006-01-02 15:04:05 MST" }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.CommitHash={{ .ShortCommit }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.GoVersion={{ .Env.GOVERSION }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.BuildID={{ .Env.GITHUB_RUN_ID }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.BuildUser={{ .Env.USER }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.BuildHost={{ .Env.HOSTNAME }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.Version={{ .Version }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.BuildTime={{ .Now.Format "2006-01-02 15:04:05 MST" }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.CommitHash={{ .ShortCommit }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.GoVersion={{ .Env.GOVERSION }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.BuildID={{ .Env.GITHUB_RUN_ID }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.BuildUser={{ .Env.USER }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.BuildHost={{ .Env.HOSTNAME }}'
|
||||
- id: cli
|
||||
main: ./cmd/cli
|
||||
binary: cli
|
||||
@@ -39,13 +39,13 @@ builds:
|
||||
- -buildvcs=false
|
||||
ldflags:
|
||||
- -s -w --extldflags "-static"
|
||||
- -X 'github.com/acepanel/panel/internal/app.Version={{ .Version }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.BuildTime={{ .Now.Format "2006-01-02 15:04:05 MST" }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.CommitHash={{ .ShortCommit }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.GoVersion={{ .Env.GOVERSION }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.BuildID={{ .Env.GITHUB_RUN_ID }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.BuildUser={{ .Env.USER }}'
|
||||
- -X 'github.com/acepanel/panel/internal/app.BuildHost={{ .Env.HOSTNAME }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.Version={{ .Version }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.BuildTime={{ .Now.Format "2006-01-02 15:04:05 MST" }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.CommitHash={{ .ShortCommit }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.GoVersion={{ .Env.GOVERSION }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.BuildID={{ .Env.GITHUB_RUN_ID }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.BuildUser={{ .Env.USER }}'
|
||||
- -X 'github.com/acepanel/panel/v3/internal/app.BuildHost={{ .Env.HOSTNAME }}'
|
||||
|
||||
archives:
|
||||
- id: panel
|
||||
|
||||
+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/internal/biz:
|
||||
config:
|
||||
recursive: True
|
||||
github.com/acepanel/panel/v3/internal/biz: {}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
## 项目概述
|
||||
|
||||
AcePanel 是基于 Go 语言开发的新一代 Linux 服务器运维管理面板。项目采用前后端分离架构:
|
||||
|
||||
- 后端:Go 1.26 + go-chi 路由 + GORM + Wire 依赖注入
|
||||
- 前端:Vue 3 + Vite + Pinia + Naive UI + pnpm + xterm.js + Alova.js
|
||||
|
||||
## 核心原则
|
||||
|
||||
- **效率至上**:快速单元式开发,所有代码注释、文档和回复使用简体中文
|
||||
- **不写文档**:只写代码,不创建 README、GUIDE 等各种文档
|
||||
- **改完即退**:完成代码修改后立即退出,用户会手动测试
|
||||
- **简洁执念**:消除边界情况永远优于增加条件判断,复杂性是万恶之源
|
||||
- **实用主义**:解决实际问题,而不是假想的威胁
|
||||
- **闭嘴**:非用户要求不输出任何内容,静默改完代码后直接退出
|
||||
|
||||
## 构建和测试
|
||||
|
||||
### 后端构建
|
||||
|
||||
构建主程序:
|
||||
|
||||
```bash
|
||||
go build -o ace ./cmd/ace
|
||||
```
|
||||
|
||||
构建 CLI 工具:
|
||||
|
||||
```bash
|
||||
go build -o cli ./cmd/cli
|
||||
```
|
||||
|
||||
### 前端构建和测试
|
||||
|
||||
进入前端目录:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
```
|
||||
|
||||
安装依赖:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
开发模式(带热重载):
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
构建生产版本:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 代码架构
|
||||
|
||||
项目采用类 DDD 分层架构,依赖关系为:route -> service -> biz <- data
|
||||
|
||||
### 核心目录结构
|
||||
|
||||
- **`cmd/`**: 程序入口
|
||||
- `ace/`: 面板主程序
|
||||
- `cli/`: 命令行工具
|
||||
|
||||
- **`internal/app/`**: 应用入口和配置
|
||||
|
||||
- **`internal/route/`**: HTTP 路由定义
|
||||
- 定义路由规则
|
||||
- 注入所需的 service 依赖
|
||||
|
||||
- **`internal/service/`**: 服务层(类似 DDD 的 application 层)
|
||||
- 处理 HTTP 请求/响应
|
||||
- DTO 到 DO 的转换
|
||||
- 协调多个 biz 接口完成业务流程
|
||||
- **不应处理复杂业务逻辑**
|
||||
|
||||
- **`internal/biz/`**: 业务逻辑层(类似 DDD 的 domain 层)
|
||||
- 定义业务接口(Repository 模式)
|
||||
- 定义领域模型和数据结构
|
||||
- 使用依赖倒置原则:biz 定义接口,data 实现接口
|
||||
|
||||
- **`internal/data/`**: 数据访问层(类似 DDD 的 repository 层)
|
||||
- 实现 biz 中定义的业务接口
|
||||
- 封装数据库、缓存等操作
|
||||
- 处理数据持久化逻辑
|
||||
|
||||
- **`internal/http/`**: HTTP 相关
|
||||
- `middleware/`: 自定义中间件
|
||||
- `request/`: 请求结构体定义
|
||||
- `rule/`: 自定义验证规则
|
||||
|
||||
- **`internal/apps/`**: 面板子应用实现
|
||||
|
||||
- **`internal/bootstrap/`**: 各模块启动引导
|
||||
|
||||
- **`internal/migration/`**: 数据库迁移
|
||||
|
||||
- **`internal/job/`**: 定时任务
|
||||
|
||||
- **`internal/taskqueue/`**: 任务队列运行器(基于 DB 轮询,实现 `types.TaskRunner` 接口)
|
||||
|
||||
- **`pkg/`**: 工具函数和通用包
|
||||
- 包含各种独立的工具模块
|
||||
- 可被项目任何部分引用
|
||||
|
||||
- **`web/`**: Vue 3 前端项目
|
||||
|
||||
## 开发新功能的标准流程
|
||||
|
||||
1. **在 `internal/route/` 中添加路由**
|
||||
- 参考已有路由文件(如 `http.go`)
|
||||
- 注入需要的 service 依赖
|
||||
- 定义路由规则和 handler 映射
|
||||
|
||||
2. **在 `internal/service/` 中实现服务方法**
|
||||
- **先阅读已有的类似服务**以了解代码风格
|
||||
- 处理请求验证和响应格式化
|
||||
- 使用 `Success()` 返回成功响应
|
||||
- 使用 `Error()` 返回错误响应
|
||||
- 使用 `ErrorSystem()` 返回系统严重错误
|
||||
- 调用 biz 层接口完成业务逻辑
|
||||
|
||||
3. **在 `internal/biz/` 中定义业务接口**
|
||||
- **先阅读已有的类似接口定义**
|
||||
- 定义 Repository 接口(如 `WebsiteRepo`)
|
||||
- 定义领域模型结构体(如 `Website`)
|
||||
- 保持接口简洁明确
|
||||
|
||||
4. **在 `internal/data/` 中实现 biz 接口**
|
||||
- **先阅读已有的类似实现**
|
||||
- 创建 repo 结构体(如 `websiteRepo`)
|
||||
- 实现构造函数(如 `NewWebsiteRepo`)
|
||||
- 实现所有接口方法
|
||||
- 处理数据库操作和缓存逻辑
|
||||
|
||||
5. **使用 Wire 进行依赖注入**
|
||||
- 在对应的 wire.go 文件中添加 provider
|
||||
- 运行 `go generate` 生成依赖注入代码
|
||||
|
||||
## 技术栈特定注意事项
|
||||
|
||||
### 助手函数(service 层)
|
||||
|
||||
在 service 层使用以下助手函数:
|
||||
|
||||
- `Success(w, data)`: 返回成功响应
|
||||
- `Error(w, statusCode, format, args...)`: 返回错误响应
|
||||
- `ErrorSystem(w, format, args...)`: 返回系统严重错误(500)
|
||||
- `Bind[T](r)`: 绑定请求参数到泛型类型 T
|
||||
- `Paginate[T](...)`: 构建分页响应
|
||||
|
||||
## 代码风格
|
||||
|
||||
- 复杂逻辑添加注释说明,简单逻辑不添加注释
|
||||
- 使用 `github.com/samber/lo` 进行函数式编程辅助
|
||||
- 后端对外返回的字符串需要尽可能使用 gotext 进行翻译处理
|
||||
- 前端使用 gettext 进行国际化处理,所有用户可见的字符串都必须使用 gettext 包裹以支持翻译
|
||||
- 禁止手动编辑前后端的翻译文件,项目由外部 Crowdin 自动化管理
|
||||
- 前端 HTTP 请求使用 Alova.js 的 useRequest 等助手函数,无需添加 onError 错误处理
|
||||
- 后端使用 Wire 依赖注入,添加新依赖时需要 `go generate ./...` 重新生成代码
|
||||
- 无需关注命令注入,SQL注入,文件上传等安全问题(这是服务器面板,所有登录的用户都被视为管理员)
|
||||
|
||||
|
||||
## 配置文件
|
||||
|
||||
后端开发配置:
|
||||
|
||||
```bash
|
||||
cp config.example.yml config.yml
|
||||
```
|
||||
|
||||
前端开发配置:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
cp .env.production .env
|
||||
cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
```
|
||||
|
||||
## 工具使用
|
||||
|
||||
对于不熟悉的库或功能,必须在修改代码前先使用以下工具进行调研:
|
||||
|
||||
1. **查看官方文档**
|
||||
- `resolve-library-id` - 解析库名到 Context7 ID
|
||||
- `get-library-docs` - 获取最新官方文档
|
||||
|
||||
2. **搜索真实代码**
|
||||
- `searchGitHub` - 搜索 GitHub 上的实际使用案例
|
||||
@@ -1,210 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## 项目概述
|
||||
|
||||
AcePanel 是基于 Go 语言开发的新一代 Linux 服务器运维管理面板。项目采用前后端分离架构:
|
||||
- 后端:Go 1.25 + go-chi 路由 + GORM + Wire 依赖注入
|
||||
- 前端:Vue 3 + Vite + Naive UI + pnpm
|
||||
|
||||
## 语言和编码规范
|
||||
|
||||
**所有代码注释、文档和回复必须使用简体中文。**
|
||||
|
||||
## 构建和测试
|
||||
|
||||
### 后端构建
|
||||
|
||||
构建主程序:
|
||||
```bash
|
||||
go build -o ace ./cmd/ace
|
||||
```
|
||||
|
||||
构建 CLI 工具:
|
||||
```bash
|
||||
go build -o cli ./cmd/cli
|
||||
```
|
||||
|
||||
### 前端开发
|
||||
|
||||
进入前端目录:
|
||||
```bash
|
||||
cd web
|
||||
```
|
||||
|
||||
安装依赖:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
开发模式(带热重载):
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
构建生产版本:
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 代码架构
|
||||
|
||||
项目采用类 DDD 分层架构,依赖关系为:route -> service -> biz <- data
|
||||
|
||||
### 核心目录结构
|
||||
|
||||
- **`cmd/`**: 程序入口
|
||||
- `ace/`: 面板主程序
|
||||
- `cli/`: 命令行工具
|
||||
|
||||
- **`internal/app/`**: 应用入口和配置
|
||||
|
||||
- **`internal/route/`**: HTTP 路由定义
|
||||
- 定义路由规则
|
||||
- 注入所需的 service 依赖
|
||||
|
||||
- **`internal/service/`**: 服务层(类似 DDD 的 application 层)
|
||||
- 处理 HTTP 请求/响应
|
||||
- DTO 到 DO 的转换
|
||||
- 协调多个 biz 接口完成业务流程
|
||||
- **不应处理复杂业务逻辑**
|
||||
|
||||
- **`internal/biz/`**: 业务逻辑层(类似 DDD 的 domain 层)
|
||||
- 定义业务接口(Repository 模式)
|
||||
- 定义领域模型和数据结构
|
||||
- 使用依赖倒置原则:biz 定义接口,data 实现接口
|
||||
|
||||
- **`internal/data/`**: 数据访问层(类似 DDD 的 repository 层)
|
||||
- 实现 biz 中定义的业务接口
|
||||
- 封装数据库、缓存等操作
|
||||
- 处理数据持久化逻辑
|
||||
|
||||
- **`internal/http/`**: HTTP 相关
|
||||
- `middleware/`: 自定义中间件
|
||||
- `request/`: 请求结构体定义
|
||||
- `rule/`: 自定义验证规则
|
||||
|
||||
- **`internal/apps/`**: 面板子应用实现
|
||||
|
||||
- **`internal/bootstrap/`**: 各模块启动引导
|
||||
|
||||
- **`internal/migration/`**: 数据库迁移
|
||||
|
||||
- **`internal/job/`**: 后台任务
|
||||
|
||||
- **`internal/queuejob/`**: 任务队列
|
||||
|
||||
- **`pkg/`**: 工具函数和通用包
|
||||
- 包含各种独立的工具模块
|
||||
- 可被项目任何部分引用
|
||||
|
||||
- **`web/`**: Vue 3 前端项目
|
||||
|
||||
## 开发新功能的标准流程
|
||||
|
||||
1. **在 `internal/route/` 中添加路由**
|
||||
- 参考已有路由文件(如 `http.go`)
|
||||
- 注入需要的 service 依赖
|
||||
- 定义路由规则和 handler 映射
|
||||
|
||||
2. **在 `internal/service/` 中实现服务方法**
|
||||
- **先阅读已有的类似服务**以了解代码风格
|
||||
- 处理请求验证和响应格式化
|
||||
- 使用 `Success()` 返回成功响应
|
||||
- 使用 `Error()` 返回错误响应
|
||||
- 使用 `ErrorSystem()` 返回系统严重错误
|
||||
- 调用 biz 层接口完成业务逻辑
|
||||
|
||||
3. **在 `internal/biz/` 中定义业务接口**
|
||||
- **先阅读已有的类似接口定义**
|
||||
- 定义 Repository 接口(如 `WebsiteRepo`)
|
||||
- 定义领域模型结构体(如 `Website`)
|
||||
- 保持接口简洁明确
|
||||
|
||||
4. **在 `internal/data/` 中实现 biz 接口**
|
||||
- **先阅读已有的类似实现**
|
||||
- 创建 repo 结构体(如 `websiteRepo`)
|
||||
- 实现构造函数(如 `NewWebsiteRepo`)
|
||||
- 实现所有接口方法
|
||||
- 处理数据库操作和缓存逻辑
|
||||
|
||||
5. **使用 Wire 进行依赖注入**
|
||||
- 在对应的 wire.go 文件中添加 provider
|
||||
- 运行 `go generate` 生成依赖注入代码
|
||||
|
||||
## 技术栈特定注意事项
|
||||
|
||||
### 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 层使用以下助手函数:
|
||||
- `Success(w, data)`: 返回成功响应
|
||||
- `Error(w, statusCode, format, args...)`: 返回错误响应
|
||||
- `ErrorSystem(w, format, args...)`: 返回系统严重错误(500)
|
||||
- `Bind[T](r)`: 绑定请求参数到泛型类型 T
|
||||
- `Paginate[T](...)`: 构建分页响应
|
||||
|
||||
### 数据库
|
||||
|
||||
- 使用 SQLite(`github.com/ncruces/go-sqlite3`)
|
||||
- 使用 GORM 进行数据库迁移和操作
|
||||
|
||||
### 安全性
|
||||
|
||||
- 不需要实现命令注入过滤,文件名过滤等,因为这是服务器面板,所有登录的用户都被视为管理员
|
||||
|
||||
## 代码风格
|
||||
|
||||
- 所有代码注释必须使用简体中文
|
||||
- 遵循 Go 官方代码风格
|
||||
- 使用 `gofmt` 格式化代码
|
||||
- 复杂逻辑添加注释说明
|
||||
- 导出的函数和类型必须有注释
|
||||
|
||||
## 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
|
||||
- 遵循项目已有的组件结构和编码风格
|
||||
|
||||
## 配置文件
|
||||
|
||||
开发时需要准备配置文件:
|
||||
```bash
|
||||
cp config.example.yml config.yml
|
||||
```
|
||||
|
||||
前端开发配置:
|
||||
```bash
|
||||
cd web
|
||||
cp .env.production .env
|
||||
cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
```
|
||||
@@ -17,7 +17,7 @@
|
||||
[](https://go.dev/)
|
||||
[](https://github.com/acepanel/panel/releases)
|
||||
[](https://github.com/acepanel/panel/actions)
|
||||
[](https://goreportcard.com/report/github.com/acepanel/panel)
|
||||
[](https://goreportcard.com/report/github.com/acepanel/panel/v3)
|
||||
[](https://github.com/acepanel/panel)
|
||||
[](https://opensource.org/license/bsd-3-clause)
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
bash <(curl -sSLm 10 https://dl.acepanel.net/helper.sh)
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> 耗子面板 2.x -> AcePanel 3.0 迁移说明:[https://tom.moe/t/topic/3540](https://tom.moe/t/topic/3540)
|
||||
|
||||
## UI 截图
|
||||
|
||||

|
||||
@@ -57,6 +60,9 @@ bash <(curl -sSLm 10 https://dl.acepanel.net/helper.sh)
|
||||
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
|
||||
<img height="60" src=".github/assets/dk.png" alt="林枫云">
|
||||
</a>
|
||||
<a href="https://cloud.panguidc.com/aff/DMRRFVJX">
|
||||
<img height="60" src=".github/assets/pangu.png" alt="盘古云">
|
||||
</a>
|
||||
<a href="https://waf.pro/">
|
||||
<img height="60" src=".github/assets/wafpro.png" alt="WAFPRO">
|
||||
</a>
|
||||
@@ -76,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>
|
||||
|
||||
+4
-1
@@ -17,7 +17,7 @@ Website: [acepanel.net](https://acepanel.net) | QQ group: [12370907](https://jq.
|
||||
[](https://go.dev/)
|
||||
[](https://github.com/acepanel/panel/releases)
|
||||
[](https://github.com/acepanel/panel/actions)
|
||||
[](https://goreportcard.com/report/github.com/acepanel/panel)
|
||||
[](https://goreportcard.com/report/github.com/acepanel/panel/v3)
|
||||
[](https://github.com/acepanel/panel)
|
||||
[](https://opensource.org/license/bsd-3-clause)
|
||||
|
||||
@@ -57,6 +57,9 @@ If the AcePanel is helpful to you, welcome to [sponsor us](https://github.com/ac
|
||||
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
|
||||
<img height="60" src=".github/assets/dk.png" alt="林枫云">
|
||||
</a>
|
||||
<a href="https://cloud.panguidc.com/aff/DMRRFVJX">
|
||||
<img height="60" src=".github/assets/pangu.png" alt="盘古云">
|
||||
</a>
|
||||
<a href="https://waf.pro/">
|
||||
<img height="60" src=".github/assets/wafpro.png" alt="WAFPRO">
|
||||
</a>
|
||||
|
||||
+8
-7
@@ -2,29 +2,30 @@
|
||||
|
||||
| Version | Supported |
|
||||
|---------|--------------------|
|
||||
| v3.x | :white_check_mark: |
|
||||
| v2.x | :white_check_mark: |
|
||||
| v1.x | :x: |
|
||||
|
||||
## Security Policy
|
||||
|
||||
If you find any security issues while using the panel, please do not submit an Issue. You can contact us directly through the following methods:
|
||||
If you find any security issues while using AcePanel, please do not submit an Issue. You can contact us directly through the following methods:
|
||||
|
||||
- (Recommend) [GitHub Security Advisories](https://github.com/acepanel/panel/security/advisories/new)
|
||||
- Email: [admin@haozi.net](mailto:admin@haozi.net)
|
||||
- Telegram: @devhaozi
|
||||
|
||||
Thank you in advance for your support and help!
|
||||
Please do not apply for CVE, CNVD, or similar vulnerability IDs without contacting us first. Once an issue has been confirmed and fixed, we will assist with the related disclosure process. Thank you in advance for your support and cooperation!
|
||||
|
||||
To some security beginners: Any operation performed through an already logged-in panel's `session` / `access_token` (including but not limited to: obtaining root permissions, reading/writing sensitive system files, executing arbitrary shell commands, etc.) is not considered a security issue. Please do not waste each other's time by submitting such reports.
|
||||
**Special Note:** AcePanel is designed with high-privilege operational capabilities. Any action performed under a valid authenticated session (such as `session` or `access_token`), including but not limited to obtaining root privileges, reading or writing sensitive system files, or executing arbitrary shell commands, is considered within the intended design scope and does not constitute a security issue. Please do not submit reports of this nature, as they are effectively self-compromise scenarios and only waste time for both parties.
|
||||
|
||||
## 安全说明
|
||||
|
||||
如果您在面板中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
|
||||
如果您在 AcePanel 中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
|
||||
|
||||
- (推荐)[GitHub 安全公告](https://github.com/acepanel/panel/security/advisories/new)
|
||||
- (推荐)[GitHub Security Advisories](https://github.com/acepanel/panel/security/advisories/new)
|
||||
- 邮箱:[admin@haozi.net](mailto:admin@haozi.net)
|
||||
- QQ:826896000
|
||||
|
||||
提前感谢您的支持与帮助!
|
||||
请勿在未与我们沟通的情况下直接申请 CVE、CNVD 等漏洞编号,相关问题确认并修复后我们会协助处理。提前感谢您的支持与配合!
|
||||
|
||||
致某些安全初学者:通过已登录面板的 `session` / `access_token` 执行的任何操作(包括且不限于:获取 root 权限、读取/写入系统敏感文件、执行任意 shell 命令等)均不被认为是安全问题,请不要刷此类报告浪费彼此的时间。
|
||||
**特别说明:** AcePanel 本身具备高权限操作能力,基于有效登录态(如 `session`、`access_token`)所执行的任何操作(包括且不限于:获取 root 权限、读取/写入系统敏感文件、执行任意 shell 命令等)均属于预期设计范围,不视为安全问题。请勿提交此类我攻击我自己的报告浪费彼此时间。
|
||||
|
||||
+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(128 << 20)
|
||||
|
||||
web, err := initWeb()
|
||||
ace, cleanup, err := initAce()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
if cleanup != nil {
|
||||
defer cleanup()
|
||||
}
|
||||
|
||||
if err = web.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ace.Run()
|
||||
}
|
||||
|
||||
+19
-11
@@ -5,17 +5,25 @@ package main
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/apps"
|
||||
"github.com/acepanel/panel/internal/bootstrap"
|
||||
"github.com/acepanel/panel/internal/data"
|
||||
"github.com/acepanel/panel/internal/http/middleware"
|
||||
"github.com/acepanel/panel/internal/job"
|
||||
"github.com/acepanel/panel/internal/route"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"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/job"
|
||||
"github.com/acepanel/panel/v3/internal/route"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
)
|
||||
|
||||
// initWeb init application.
|
||||
func initWeb() (*app.Web, error) {
|
||||
panic(wire.Build(bootstrap.ProviderSet, middleware.ProviderSet, route.ProviderSet, service.ProviderSet, data.ProviderSet, apps.ProviderSet, job.ProviderSet, app.NewWeb))
|
||||
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,
|
||||
))
|
||||
}
|
||||
|
||||
+333
-138
@@ -7,34 +7,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/apps/apache"
|
||||
"github.com/acepanel/panel/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/internal/apps/docker"
|
||||
"github.com/acepanel/panel/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/internal/apps/frp"
|
||||
"github.com/acepanel/panel/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/internal/apps/minio"
|
||||
"github.com/acepanel/panel/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/internal/apps/percona"
|
||||
"github.com/acepanel/panel/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/internal/apps/podman"
|
||||
"github.com/acepanel/panel/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/internal/apps/redis"
|
||||
"github.com/acepanel/panel/internal/apps/rsync"
|
||||
"github.com/acepanel/panel/internal/apps/s3fs"
|
||||
"github.com/acepanel/panel/internal/apps/supervisor"
|
||||
"github.com/acepanel/panel/internal/bootstrap"
|
||||
"github.com/acepanel/panel/internal/data"
|
||||
"github.com/acepanel/panel/internal/http/middleware"
|
||||
"github.com/acepanel/panel/internal/job"
|
||||
"github.com/acepanel/panel/internal/route"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"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/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"
|
||||
"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/data"
|
||||
"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"
|
||||
)
|
||||
|
||||
import (
|
||||
@@ -43,139 +55,322 @@ import (
|
||||
|
||||
// Injectors from wire.go:
|
||||
|
||||
// initWeb init application.
|
||||
func initWeb() (*app.Web, 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)
|
||||
queue := bootstrap.NewQueue()
|
||||
taskRepo := data.NewTaskRepo(locale, db, logger, queue)
|
||||
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)
|
||||
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)
|
||||
homeService := service.NewHomeService(locale, config, taskRepo, websiteRepo, projectRepo, appRepo, environmentRepo, settingRepo, cronRepo, backupRepo)
|
||||
taskService := service.NewTaskService(taskRepo)
|
||||
websiteService := service.NewWebsiteService(websiteRepo, settingRepo)
|
||||
projectService := service.NewProjectService(projectRepo, settingRepo)
|
||||
databaseService := service.NewDatabaseService(databaseRepo)
|
||||
databaseServerService := service.NewDatabaseServerService(databaseServerRepo)
|
||||
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)
|
||||
cronService := service.NewCronService(cronRepo)
|
||||
processService := service.NewProcessService()
|
||||
safeRepo := data.NewSafeRepo(logger)
|
||||
safeService := service.NewSafeService(safeRepo)
|
||||
firewallService := service.NewFirewallService()
|
||||
sshRepo := data.NewSSHRepo(locale, db, logger)
|
||||
sshService := service.NewSSHService(sshRepo)
|
||||
containerRepo := data.NewContainerRepo()
|
||||
containerService := service.NewContainerService(containerRepo)
|
||||
containerComposeRepo := data.NewContainerComposeRepo()
|
||||
containerComposeService := service.NewContainerComposeService(containerComposeRepo)
|
||||
containerNetworkRepo := data.NewContainerNetworkRepo()
|
||||
containerNetworkService := service.NewContainerNetworkService(containerNetworkRepo)
|
||||
containerImageRepo := data.NewContainerImageRepo()
|
||||
containerImageService := service.NewContainerImageService(containerImageRepo)
|
||||
containerVolumeRepo := data.NewContainerVolumeRepo()
|
||||
containerVolumeService := service.NewContainerVolumeService(containerVolumeRepo)
|
||||
fileService := service.NewFileService(locale, taskRepo)
|
||||
logRepo := data.NewLogRepo(db)
|
||||
logService := service.NewLogService(logRepo)
|
||||
monitorRepo := data.NewMonitorRepo(db, settingRepo)
|
||||
monitorService := service.NewMonitorService(settingRepo, monitorRepo)
|
||||
settingService := service.NewSettingService(locale, db, settingRepo, certRepo, certAccountRepo)
|
||||
systemctlService := service.NewSystemctlService(locale)
|
||||
toolboxSystemService := service.NewToolboxSystemService(locale)
|
||||
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)
|
||||
toolboxSSHService := service.NewToolboxSSHService(locale)
|
||||
toolboxDiskService := service.NewToolboxDiskService(locale)
|
||||
toolboxLogService := service.NewToolboxLogService(locale, db, containerImageRepo, settingRepo)
|
||||
webHookRepo := data.NewWebHookRepo(locale, db, logger)
|
||||
webHookService := service.NewWebHookService(webHookRepo)
|
||||
templateRepo := data.NewTemplateRepo(locale, cacheRepo)
|
||||
templateService := service.NewTemplateService(locale, templateRepo, settingRepo)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
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()
|
||||
mariadbApp := mariadb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
grafanaApp := grafana.NewApp(locale)
|
||||
kafkaApp := kafka.NewApp(locale)
|
||||
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()
|
||||
mysqlApp := mysql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
mongodbApp := mongodb.NewApp(locale, databaseServerRepo, settingRepo)
|
||||
nginxApp := nginx.NewApp(locale)
|
||||
openrestyApp := openresty.NewApp(locale)
|
||||
perconaApp := percona.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
phpmyadminApp := phpmyadmin.NewApp(locale)
|
||||
openrestyApp := openresty.NewApp(nginxApp)
|
||||
opensearchApp := opensearch.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)
|
||||
postgresqlApp := postgresql.NewApp(locale, config, databaseServerRepo, settingRepo, taskRepo)
|
||||
prometheusApp := prometheus.NewApp(config, locale, taskRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo, taskRepo)
|
||||
rocketmqApp := rocketmq.NewApp(locale)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, backupStorageService, certService, certDNSService, certAccountService, appService, environmentService, environmentGoService, environmentJavaService, environmentNodejsService, environmentPHPService, environmentPythonService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, webHookService, templateService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo)
|
||||
ws := route.NewWs(wsService)
|
||||
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
|
||||
}
|
||||
server, err := bootstrap.NewHttp(config, mux)
|
||||
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
|
||||
}
|
||||
validator := bootstrap.NewValidator(config, db)
|
||||
alertRepo, err := data.NewAlertRepo(db)
|
||||
if err != nil {
|
||||
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, settingRepo, certRepo, certAccountRepo, backupRepo, cacheRepo, taskRepo)
|
||||
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)
|
||||
web := app.NewWeb(config, mux, server, gormigrate, cron, queue, validation)
|
||||
return web, 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()
|
||||
}
|
||||
|
||||
+15
-9
@@ -5,15 +5,21 @@ package main
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/apps"
|
||||
"github.com/acepanel/panel/internal/bootstrap"
|
||||
"github.com/acepanel/panel/internal/data"
|
||||
"github.com/acepanel/panel/internal/route"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"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/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,
|
||||
))
|
||||
}
|
||||
|
||||
+64
-74
@@ -7,32 +7,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/apps/apache"
|
||||
"github.com/acepanel/panel/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/internal/apps/docker"
|
||||
"github.com/acepanel/panel/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/internal/apps/frp"
|
||||
"github.com/acepanel/panel/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/internal/apps/minio"
|
||||
"github.com/acepanel/panel/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/internal/apps/percona"
|
||||
"github.com/acepanel/panel/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/internal/apps/podman"
|
||||
"github.com/acepanel/panel/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/internal/apps/redis"
|
||||
"github.com/acepanel/panel/internal/apps/rsync"
|
||||
"github.com/acepanel/panel/internal/apps/s3fs"
|
||||
"github.com/acepanel/panel/internal/apps/supervisor"
|
||||
"github.com/acepanel/panel/internal/bootstrap"
|
||||
"github.com/acepanel/panel/internal/data"
|
||||
"github.com/acepanel/panel/internal/route"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"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/service"
|
||||
)
|
||||
|
||||
import (
|
||||
@@ -41,60 +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)
|
||||
queue := bootstrap.NewQueue()
|
||||
taskRepo := data.NewTaskRepo(locale, db, logger, queue)
|
||||
appRepo := data.NewAppRepo(locale, config, db, logger, cacheRepo, taskRepo)
|
||||
userRepo := data.NewUserRepo(locale, db, logger)
|
||||
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, settingRepo, backupRepo, websiteRepo, databaseServerRepo, certRepo, certAccountRepo)
|
||||
cli := route.NewCli(locale, cliService)
|
||||
command := bootstrap.NewCli(locale, cli)
|
||||
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)
|
||||
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)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
dockerApp := docker.NewApp()
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
frpApp := frp.NewApp()
|
||||
giteaApp := gitea.NewApp()
|
||||
mariadbApp := mariadb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
memcachedApp := memcached.NewApp(locale)
|
||||
minioApp := minio.NewApp()
|
||||
mysqlApp := mysql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
nginxApp := nginx.NewApp(locale)
|
||||
openrestyApp := openresty.NewApp(locale)
|
||||
perconaApp := percona.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
phpmyadminApp := phpmyadmin.NewApp(locale)
|
||||
podmanApp := podman.NewApp()
|
||||
postgresqlApp := postgresql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
appCli := app.NewCli(command, gormigrate, loader)
|
||||
return appCli, nil
|
||||
cli := app.NewCli(cliCommand, gormigrate)
|
||||
return cli, func() {
|
||||
cleanup()
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,115 +1,154 @@
|
||||
module github.com/acepanel/panel
|
||||
module github.com/acepanel/panel/v3
|
||||
|
||||
go 1.25.6
|
||||
go 1.27.0
|
||||
|
||||
require (
|
||||
github.com/DeRuina/timberjack v1.3.9
|
||||
github.com/bddjr/hlfhr v1.4.0
|
||||
github.com/andybalholm/brotli v1.2.3
|
||||
github.com/bddjr/hlfhr v1.6.1
|
||||
github.com/beevik/ntp v1.5.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.6.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/expr-lang/expr v1.17.7
|
||||
github.com/go-chi/chi/v5 v5.2.4
|
||||
github.com/go-chi/httplog/v3 v3.3.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-resty/resty/v2 v2.17.1
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/expr-lang/expr v1.17.8
|
||||
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.6
|
||||
github.com/hashicorp/go-version v1.8.0
|
||||
github.com/klauspost/compress v1.18.3
|
||||
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.10.9
|
||||
github.com/libdns/alidns v1.0.6-beta.3
|
||||
github.com/lib/pq v1.12.3
|
||||
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.0
|
||||
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/chix v1.3.2
|
||||
github.com/libtnb/gormstore v1.1.1
|
||||
github.com/libtnb/sessions v1.2.2
|
||||
github.com/libtnb/utils v1.2.1
|
||||
github.com/mholt/acmez/v3 v3.1.4
|
||||
github.com/moby/moby/api v1.53.0-rc.2
|
||||
github.com/moby/moby/client v0.2.1
|
||||
github.com/ncruces/go-sqlite3 v0.30.5
|
||||
github.com/ncruces/go-sqlite3/gormlite v0.30.2
|
||||
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.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.52.0
|
||||
github.com/sethvargo/go-limiter v1.1.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.12
|
||||
github.com/samber/lo v1.53.0
|
||||
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/tufanbarisyildirim/gonginx v0.0.0-20250620092546-c3e307e36701
|
||||
github.com/urfave/cli/v3 v3.6.2
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/net v0.49.0
|
||||
gorm.io/gorm v1.31.1
|
||||
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.11.0
|
||||
github.com/valyala/fastjson v1.6.10
|
||||
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.0 // 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/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // 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/gofiber/schema v1.6.0 // indirect
|
||||
github.com/gookit/filter v1.2.3 // indirect
|
||||
github.com/gookit/goutil v0.7.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.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/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/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.24 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/ncruces/julianday v1.0.0 // 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/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/tetratelabs/wazero v1.11.0 // 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.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.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.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-20260103184942-a835890fc93e
|
||||
github.com/moby/moby/client => github.com/libtnb/moby/client v0.0.0-20260119133723-7d7dd88cf643
|
||||
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.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/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.3.9 h1:6UXZ1I7ExPGTX/1UNYawR58LlOJUHKBPiYC7WQ91eBo=
|
||||
github.com/DeRuina/timberjack v1.3.9/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,11 +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.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.4.0 h1:EVryUs0mLzQ455bAIKhASqwxFV9IYrGOFjuz0HE6oYE=
|
||||
github.com/bddjr/hlfhr v1.4.0/go.mod h1:oyIv4Q9JpCgZFdtH3KyTNWp7YYRWl4zl8k4ozrMAB4g=
|
||||
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=
|
||||
@@ -38,10 +42,14 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
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.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=
|
||||
@@ -50,8 +58,8 @@ github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkE
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
@@ -59,51 +67,72 @@ 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/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
|
||||
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
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.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/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.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
|
||||
github.com/go-chi/chi/v5 v5.2.4/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-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
|
||||
github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
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-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.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/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-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.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=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
@@ -120,26 +149,28 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/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/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-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=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gookit/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.3 h1:nXDd/AB17nEjqVCNDGioDhVL/gVqdlqRMfFergKDjHE=
|
||||
github.com/gookit/goutil v0.7.3/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/gookit/validate v1.5.6 h1:D6vbSZzreuKYpeeXm5FDDEJy3K5E4lcWsQE4saSMZbU=
|
||||
github.com/gookit/validate v1.5.6/go.mod h1:WYEHndRNepIIkM+6CtgEX9MQ9ToIQRhXxmz5oLHF/fc=
|
||||
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=
|
||||
@@ -157,11 +188,14 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
@@ -179,14 +213,18 @@ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
|
||||
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/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=
|
||||
@@ -200,51 +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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/libdns/alidns v1.0.6-beta.3 h1:KAmb7FQ1tRzKsaAUGa7ZpGKAMRANwg7+1c7tUbSELq8=
|
||||
github.com/libdns/alidns v1.0.6-beta.3/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
|
||||
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.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.0 h1:BQUIkOAjF++ouiANRIE3jdMlCfeiAAr6tGqb+8hM1jo=
|
||||
github.com/libdns/huaweicloud v1.0.0/go.mod h1:W+XywkW+C93fM50Ayklf6KuoCHZTqbrmFUEtpQnVvcU=
|
||||
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-20260103184942-a835890fc93e h1:HPHmtFcR5VpTJTpDqG4+l16uJfSlpB/X0r3qq22znrk=
|
||||
github.com/libtnb/acmez/v3 v3.0.0-20260103184942-a835890fc93e/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
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.1.1 h1:FG/3P4PuWM6/vB4weVJ31meiSaoeXns1NQlP66quKeg=
|
||||
github.com/libtnb/gormstore v1.1.1/go.mod h1:8A5QzeZxi1MpSmjUVsHTDAL6KnU84feIXMutFLPawwA=
|
||||
github.com/libtnb/moby/client v0.0.0-20260119133723-7d7dd88cf643 h1:B7uVTSsIh/9b+gpVOHWcg+8xmYf1g3E9v4OQYcPCkt8=
|
||||
github.com/libtnb/moby/client v0.0.0-20260119133723-7d7dd88cf643/go.mod h1:y8zxhZOdysHyNZgn9osm6SBDNmissvoai5HLhAdSIFo=
|
||||
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.2.2 h1:VTTzzeBDJEkJbaPaIU9C4bRj2oAqD0rgQ7UHFkkaNT4=
|
||||
github.com/libtnb/sessions v1.2.2/go.mod h1:qw+FWtBtrPDYCf6MfX0Lk5EhTArpvT72z5Ei4RUMTRg=
|
||||
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.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.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=
|
||||
@@ -255,17 +317,15 @@ 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.53.0-rc.2 h1:Ajc2MnWJRC2AAqhIGNaqvwaA2mMYKyuso5PjxPP2KM4=
|
||||
github.com/moby/moby/api v1.53.0-rc.2/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
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=
|
||||
github.com/ncruces/go-sqlite3 v0.30.5 h1:6usmTQ6khriL8oWilkAZSJM/AIpAlVL2zFrlcpDldCE=
|
||||
github.com/ncruces/go-sqlite3 v0.30.5/go.mod h1:0I0JFflTKzfs3Ogfv8erP7CCoV/Z8uxigVDNOR0AQ5E=
|
||||
github.com/ncruces/go-sqlite3/gormlite v0.30.2 h1:FZ8mic14xTatssTkHCrelh9nPeFdXuzgMoNGkfuFbBU=
|
||||
github.com/ncruces/go-sqlite3/gormlite v0.30.2/go.mod h1:W9WLBbqrrOIh2dqFZkeC/xKALG2LDIHY91jowahOdtI=
|
||||
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
|
||||
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
@@ -275,12 +335,18 @@ 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=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
@@ -295,21 +361,27 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
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=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
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.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
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=
|
||||
@@ -329,11 +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/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
|
||||
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
|
||||
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=
|
||||
@@ -342,45 +414,79 @@ 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.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=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20250620092546-c3e307e36701 h1:JgeHIJzRSEdcuLXufZrni5+a4yDnBhQG+DdKhqCFhq0=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20250620092546-c3e307e36701/go.mod h1:ALbEe81QPWOZjDKCKNWodG2iqCMtregG8+ebQgjx2+4=
|
||||
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
|
||||
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
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.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=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
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=
|
||||
@@ -392,6 +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.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=
|
||||
@@ -406,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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
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=
|
||||
@@ -416,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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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=
|
||||
@@ -434,20 +542,18 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
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/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -465,6 +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.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=
|
||||
@@ -498,14 +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.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.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.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.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.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-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=
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/bddjr/hlfhr"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
"github.com/libtnb/cron"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/tlscert"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type Ace struct {
|
||||
conf *config.Config
|
||||
router *chi.Mux
|
||||
server *hlfhr.Server
|
||||
reloader *tlscert.Reloader
|
||||
migrator *gormigrate.Gormigrate
|
||||
cron *cron.Cron
|
||||
runner types.TaskRunner
|
||||
}
|
||||
|
||||
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,
|
||||
server: server,
|
||||
reloader: reloader,
|
||||
migrator: migrator,
|
||||
cron: cron,
|
||||
runner: runner,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Ace) Run() error {
|
||||
// migrate database
|
||||
if err := r.migrator.Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("[DB] database migrated")
|
||||
|
||||
// start cron scheduler
|
||||
if err := r.cron.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("[CRON] cron scheduler started")
|
||||
|
||||
// create context for runner
|
||||
runnerCtx, runnerCancel := context.WithCancel(context.Background())
|
||||
defer runnerCancel()
|
||||
|
||||
// start task runner
|
||||
r.runner.Run(runnerCtx)
|
||||
|
||||
// setup graceful shutdown
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// run http server in goroutine
|
||||
serverErr := make(chan error, 1)
|
||||
go func() {
|
||||
fmt.Println("[HTTP] listening and serving on port", r.conf.HTTP.Port)
|
||||
if r.conf.HTTP.IsHTTPS() {
|
||||
if err := r.server.ListenAndServeTLS("", ""); !errors.Is(err, http.ErrServerClosed) {
|
||||
serverErr <- err
|
||||
}
|
||||
} else {
|
||||
if err := r.server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
serverErr <- err
|
||||
}
|
||||
}
|
||||
close(serverErr)
|
||||
}()
|
||||
|
||||
// wait for shutdown signal or server error
|
||||
select {
|
||||
case err := <-serverErr:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case sig := <-quit:
|
||||
fmt.Println("[APP] received signal:", sig)
|
||||
}
|
||||
|
||||
// graceful shutdown
|
||||
fmt.Println("[APP] shutting down gracefully...")
|
||||
|
||||
// stop cron scheduler
|
||||
cronCtx, cronCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
_ = r.cron.Stop(cronCtx)
|
||||
cronCancel()
|
||||
fmt.Println("[CRON] cron scheduler stopped")
|
||||
|
||||
// stop task runner
|
||||
runnerCancel()
|
||||
fmt.Println("[QUEUE] task runner stopped")
|
||||
|
||||
// close certificate reloader
|
||||
if r.reloader != nil {
|
||||
if err := r.reloader.Close(); err != nil {
|
||||
fmt.Println("[TLS] certificate reloader close error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// shutdown http server
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer shutdownCancel()
|
||||
|
||||
if err := r.server.Shutdown(shutdownCtx); err != nil {
|
||||
fmt.Println("[HTTP] server shutdown error:", err)
|
||||
return err
|
||||
}
|
||||
fmt.Println("[HTTP] server stopped")
|
||||
|
||||
fmt.Println("[APP] shutdown complete")
|
||||
return nil
|
||||
}
|
||||
+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/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)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package app
|
||||
|
||||
import "time"
|
||||
|
||||
// StartTime 面板启动时间
|
||||
var StartTime = time.Now()
|
||||
|
||||
// 面板状态常量
|
||||
const (
|
||||
StatusNormal = iota
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"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/acepanel/panel/pkg/config"
|
||||
"github.com/acepanel/panel/pkg/queue"
|
||||
)
|
||||
|
||||
type Web struct {
|
||||
conf *config.Config
|
||||
router *chi.Mux
|
||||
server *hlfhr.Server
|
||||
migrator *gormigrate.Gormigrate
|
||||
cron *cron.Cron
|
||||
queue *queue.Queue
|
||||
}
|
||||
|
||||
func NewWeb(conf *config.Config, router *chi.Mux, server *hlfhr.Server, migrator *gormigrate.Gormigrate, cron *cron.Cron, queue *queue.Queue, _ *validate.Validation) *Web {
|
||||
return &Web{
|
||||
conf: conf,
|
||||
router: router,
|
||||
server: server,
|
||||
migrator: migrator,
|
||||
cron: cron,
|
||||
queue: queue,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Web) Run() error {
|
||||
// migrate database
|
||||
if err := r.migrator.Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("[DB] database migrated")
|
||||
|
||||
// start cron scheduler
|
||||
r.cron.Start()
|
||||
fmt.Println("[CRON] cron scheduler started")
|
||||
|
||||
// start queue
|
||||
r.queue.Run(context.TODO())
|
||||
|
||||
// run http server
|
||||
if r.conf.HTTP.TLS {
|
||||
cert := filepath.Join(Root, "panel/storage/cert.pem")
|
||||
key := filepath.Join(Root, "panel/storage/cert.key")
|
||||
fmt.Println("[HTTP] listening and serving on port", r.conf.HTTP.Port, "with tls")
|
||||
if err := r.server.ListenAndServeTLS(cert, key); !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
fmt.Println("[HTTP] listening and serving on port", r.conf.HTTP.Port)
|
||||
if err := r.server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -9,15 +9,18 @@ import (
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"github.com/acepanel/panel/pkg/tools"
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
"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"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
"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"`
|
||||
}
|
||||
|
||||
+41
-42
@@ -3,49 +3,48 @@ package apps
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/acepanel/panel/internal/apps/apache"
|
||||
"github.com/acepanel/panel/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/internal/apps/docker"
|
||||
"github.com/acepanel/panel/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/internal/apps/frp"
|
||||
"github.com/acepanel/panel/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/internal/apps/minio"
|
||||
"github.com/acepanel/panel/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/internal/apps/nginx"
|
||||
"github.com/acepanel/panel/internal/apps/openresty"
|
||||
"github.com/acepanel/panel/internal/apps/percona"
|
||||
"github.com/acepanel/panel/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/internal/apps/podman"
|
||||
"github.com/acepanel/panel/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/internal/apps/redis"
|
||||
"github.com/acepanel/panel/internal/apps/rsync"
|
||||
"github.com/acepanel/panel/internal/apps/s3fs"
|
||||
"github.com/acepanel/panel/internal/apps/supervisor"
|
||||
"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/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"
|
||||
"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"
|
||||
)
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
apache.NewApp,
|
||||
codeserver.NewApp,
|
||||
docker.NewApp,
|
||||
fail2ban.NewApp,
|
||||
frp.NewApp,
|
||||
gitea.NewApp,
|
||||
mariadb.NewApp,
|
||||
memcached.NewApp,
|
||||
minio.NewApp,
|
||||
mysql.NewApp,
|
||||
nginx.NewApp,
|
||||
openresty.NewApp,
|
||||
percona.NewApp,
|
||||
phpmyadmin.NewApp,
|
||||
podman.NewApp,
|
||||
postgresql.NewApp,
|
||||
pureftpd.NewApp,
|
||||
redis.NewApp,
|
||||
rsync.NewApp,
|
||||
s3fs.NewApp,
|
||||
supervisor.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,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
package clickhouse
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"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"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
settingRepo biz.SettingRepo
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
r.Get("/default_password", s.GetDefaultPassword)
|
||||
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")
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
password, _ := s.settingRepo.Get(biz.SettingKeyClickHouseDefaultPassword)
|
||||
port := s.getPort()
|
||||
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
|
||||
// 获取版本
|
||||
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.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
version := strings.TrimSpace(string(versionResp.Bytes()))
|
||||
|
||||
// 获取运行时间
|
||||
uptimeResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+uptime()&user=default&password=%s", port, password))
|
||||
uptime := strings.TrimSpace(string(uptimeResp.Bytes()))
|
||||
|
||||
// 获取当前查询数
|
||||
queriesResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+value+FROM+system.metrics+WHERE+metric='Query'&user=default&password=%s", port, password))
|
||||
queries := strings.TrimSpace(string(queriesResp.Bytes()))
|
||||
|
||||
// 获取内存使用
|
||||
memResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+value+FROM+system.metrics+WHERE+metric='MemoryTracking'&user=default&password=%s", port, password))
|
||||
memUsage := strings.TrimSpace(string(memResp.Bytes()))
|
||||
|
||||
// 获取数据库数量
|
||||
dbCountResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+count()+FROM+system.databases&user=default&password=%s", port, password))
|
||||
dbCount := strings.TrimSpace(string(dbCountResp.Bytes()))
|
||||
|
||||
// 获取表数量
|
||||
tableCountResp, _ := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/?query=SELECT+count()+FROM+system.tables&user=default&password=%s", port, password))
|
||||
tableCount := strings.TrimSpace(string(tableCountResp.Bytes()))
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Version"), Value: version},
|
||||
{Name: s.t.Get("Uptime (seconds)"), Value: uptime},
|
||||
{Name: s.t.Get("Active Queries"), Value: queries},
|
||||
{Name: s.t.Get("Memory Usage (bytes)"), Value: memUsage},
|
||||
{Name: s.t.Get("Databases"), Value: dbCount},
|
||||
{Name: s.t.Get("Tables"), Value: tableCount},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.SaveConfig(w, r, s.configPath(), "clickhouse-server")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg == nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
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)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新配置调整参数
|
||||
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
|
||||
}
|
||||
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
// ClickHouse 顶层键直接用平铺方式
|
||||
s.setYAMLValue(cfg, "listen_host", req.ListenHost)
|
||||
s.setYAMLValue(cfg, "http_port", req.HTTPPort)
|
||||
s.setYAMLValue(cfg, "tcp_port", req.TCPPort)
|
||||
s.setYAMLValue(cfg, "max_memory_usage", req.MaxMemoryUsage)
|
||||
s.setYAMLValue(cfg, "max_threads", req.MaxThreads)
|
||||
s.setYAMLValue(cfg, "path", req.Path)
|
||||
s.setYAMLValue(cfg, "tmp_path", req.TmpPath)
|
||||
// logger.level 是嵌套的
|
||||
s.setNestedYAMLValue(cfg, "logger.level", req.LogLevel)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Write(s.configPath(), string(data), 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)
|
||||
}
|
||||
|
||||
// GetDefaultPassword 获取 default 用户密码
|
||||
func (s *App) GetDefaultPassword(w http.ResponseWriter, r *http.Request) {
|
||||
password, err := s.settingRepo.Get(biz.SettingKeyClickHouseDefaultPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get ClickHouse default password: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, password)
|
||||
}
|
||||
|
||||
// SetDefaultPassword 设置 default 用户密码
|
||||
func (s *App) SetDefaultPassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[SetDefaultPassword](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 计算 SHA256 哈希
|
||||
hash := sha256.Sum256([]byte(req.Password))
|
||||
hexHash := hex.EncodeToString(hash[:])
|
||||
|
||||
// 读取 users.d/default.yaml 并更新密码
|
||||
raw, _ := io.Read(s.usersConfigPath())
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
users, _ := cfg["users"].(map[string]any)
|
||||
if users == nil {
|
||||
users = make(map[string]any)
|
||||
cfg["users"] = users
|
||||
}
|
||||
def, _ := users["default"].(map[string]any)
|
||||
if def == nil {
|
||||
def = make(map[string]any)
|
||||
users["default"] = def
|
||||
}
|
||||
def["password_sha256_hex"] = hexHash
|
||||
out, _ := yaml.Marshal(cfg)
|
||||
if err = io.Write(s.usersConfigPath(), string(out), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to write ClickHouse user config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 重启服务使密码生效
|
||||
if err = systemctl.Restart("clickhouse-server"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 保存明文到面板数据库
|
||||
if err = s.settingRepo.Set(biz.SettingKeyClickHouseDefaultPassword, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to save ClickHouse default password: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_clickhouse", req.Password)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// configPath 返回主配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return app.Root + "/server/clickhouse/config/config.yaml"
|
||||
}
|
||||
|
||||
// usersConfigPath 返回用户密码配置文件路径(users.d/ 由 ConfigProcessor 自动合并到 users.yaml)
|
||||
func (s *App) usersConfigPath() string {
|
||||
return app.Root + "/server/clickhouse/config/users.d/default.yaml"
|
||||
}
|
||||
|
||||
// getPort 从配置中获取 HTTP 端口
|
||||
func (s *App) getPort() string {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := confval.GetYAML(cfg, "http_port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "8123"
|
||||
}
|
||||
|
||||
// setYAMLValue 设置平铺 YAML 值
|
||||
func (s *App) setYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
cfg[key] = value
|
||||
}
|
||||
|
||||
// setNestedYAMLValue 设置嵌套 YAML 值
|
||||
func (s *App) setNestedYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
if len(parts) == 1 {
|
||||
cfg[parts[0]] = value
|
||||
return
|
||||
}
|
||||
nested, ok := cfg[parts[0]].(map[string]any)
|
||||
if !ok {
|
||||
nested = make(map[string]any)
|
||||
cfg[parts[0]] = nested
|
||||
}
|
||||
s.setNestedYAMLValue(nested, parts[1], value)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package clickhouse
|
||||
|
||||
// SetDefaultPassword 设置 default 用户密码
|
||||
type SetDefaultPassword struct {
|
||||
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" 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" validate:"unix_path"`
|
||||
TmpPath string `form:"tmp_path" json:"tmp_path" validate:"unix_path"`
|
||||
// 日志
|
||||
LogLevel string `form:"log_level" json:"log_level" validate:"in:trace,debug,information,warning,error"`
|
||||
}
|
||||
@@ -5,9 +5,10 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"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)
|
||||
}
|
||||
}
|
||||
+16
-35
@@ -8,9 +8,11 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"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,44 +28,25 @@ 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 设置
|
||||
func (s *App) GetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
configPath := "/etc/docker/daemon.json"
|
||||
|
||||
// 读取配置文件
|
||||
content, err := io.Read(configPath)
|
||||
if err != nil {
|
||||
// 如果文件不存在,返回默认设置
|
||||
if os.IsNotExist(err) {
|
||||
service.Success(w, Settings{})
|
||||
return
|
||||
@@ -72,14 +55,12 @@ func (s *App) GetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 JSON
|
||||
var daemonConfig DaemonConfig
|
||||
if err = json.Unmarshal([]byte(content), &daemonConfig); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
service.Success(w, Settings{}) // 配置文件可能为空或格式错误,返回默认设置
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为 Settings 结构
|
||||
settings := Settings{
|
||||
RegistryMirrors: daemonConfig.RegistryMirrors,
|
||||
InsecureRegistries: daemonConfig.InsecureRegistries,
|
||||
@@ -107,8 +88,8 @@ func (s *App) GetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 从 exec-opts 中提取 cgroup-driver
|
||||
for _, opt := range daemonConfig.ExecOpts {
|
||||
if strings.HasPrefix(opt, "native.cgroupdriver=") {
|
||||
settings.CgroupDriver = strings.TrimPrefix(opt, "native.cgroupdriver=")
|
||||
if after, ok := strings.CutPrefix(opt, "native.cgroupdriver="); ok {
|
||||
settings.CgroupDriver = after
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -127,7 +108,7 @@ func (s *App) UpdateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
configPath := "/etc/docker/daemon.json"
|
||||
settings := req.Settings
|
||||
|
||||
// 读取现有配置(保留其他字段)
|
||||
// 读取现有配置
|
||||
var existingConfig map[string]any
|
||||
content, err := io.Read(configPath)
|
||||
if err == nil && content != "" {
|
||||
|
||||
@@ -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,omitempty"` // 日志配置选项
|
||||
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 更新设置请求
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package elasticsearch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"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/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 {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
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 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get elasticsearch status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
port := s.getPort()
|
||||
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.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var health struct {
|
||||
ClusterName string `json:"cluster_name"`
|
||||
Status string `json:"status"`
|
||||
NumberOfNodes int `json:"number_of_nodes"`
|
||||
NumberOfDataNodes int `json:"number_of_data_nodes"`
|
||||
ActiveShards int `json:"active_shards"`
|
||||
ActivePrimaryShards int `json:"active_primary_shards"`
|
||||
RelocatingShards int `json:"relocating_shards"`
|
||||
UnassignedShards int `json:"unassigned_shards"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.Bytes(), &health); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Cluster Name"), Value: health.ClusterName},
|
||||
{Name: s.t.Get("Cluster Status"), Value: health.Status},
|
||||
{Name: s.t.Get("Number of Nodes"), Value: cast.ToString(health.NumberOfNodes)},
|
||||
{Name: s.t.Get("Number of Data Nodes"), Value: cast.ToString(health.NumberOfDataNodes)},
|
||||
{Name: s.t.Get("Active Shards"), Value: cast.ToString(health.ActiveShards)},
|
||||
{Name: s.t.Get("Active Primary Shards"), Value: cast.ToString(health.ActivePrimaryShards)},
|
||||
{Name: s.t.Get("Relocating Shards"), Value: cast.ToString(health.RelocatingShards)},
|
||||
{Name: s.t.Get("Unassigned Shards"), Value: cast.ToString(health.UnassignedShards)},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.SaveConfig(w, r, s.configPath(), "elasticsearch")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 ElasticSearch 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg == nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
// ES 9.x 不再在 jvm.options 中设置堆内存,改为 jvm.options.d/heap.options
|
||||
heapRaw, _ := io.Read(s.jvmHeapOptionsPath())
|
||||
heapInit, heapMax := s.parseJVMHeap(heapRaw)
|
||||
// 兼容旧版本:如果 heap.options 没有,尝试从 jvm.options 读取
|
||||
if heapInit == "" && heapMax == "" {
|
||||
jvmRaw, _ := io.Read(s.jvmOptionsPath())
|
||||
heapInit, heapMax = s.parseJVMHeap(jvmRaw)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
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,
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 ElasticSearch 配置调整参数
|
||||
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
|
||||
}
|
||||
|
||||
// 更新 YAML 配置
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
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 {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Write(s.configPath(), string(data), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 JVM 堆内存(写入 jvm.options.d/heap.options)
|
||||
if req.HeapInitSize != "" || req.HeapMaxSize != "" {
|
||||
heapRaw, _ := io.Read(s.jvmHeapOptionsPath())
|
||||
heapRaw = s.setJVMHeap(heapRaw, req.HeapInitSize, req.HeapMaxSize)
|
||||
if err = io.Write(s.jvmHeapOptionsPath(), heapRaw, 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)
|
||||
}
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return app.Root + "/server/elasticsearch/config/elasticsearch.yml"
|
||||
}
|
||||
|
||||
// jvmOptionsPath 返回 JVM 选项文件路径
|
||||
func (s *App) jvmOptionsPath() string {
|
||||
return app.Root + "/server/elasticsearch/config/jvm.options"
|
||||
}
|
||||
|
||||
// jvmHeapOptionsPath 返回 JVM 堆内存配置文件路径(ES 9.x 推荐方式)
|
||||
func (s *App) jvmHeapOptionsPath() string {
|
||||
return app.Root + "/server/elasticsearch/config/jvm.options.d/heap.options"
|
||||
}
|
||||
|
||||
// getPort 从配置中获取 HTTP 端口
|
||||
func (s *App) getPort() string {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := confval.GetYAML(cfg, "http.port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "9200"
|
||||
}
|
||||
|
||||
// parseJVMHeap 从 jvm.options 中提取堆内存配置
|
||||
func (s *App) parseJVMHeap(content string) (initSize, maxSize string) {
|
||||
reInit := regexp.MustCompile(`(?m)^-Xms(\S+)`)
|
||||
reMax := regexp.MustCompile(`(?m)^-Xmx(\S+)`)
|
||||
if m := reInit.FindStringSubmatch(content); len(m) == 2 {
|
||||
initSize = m[1]
|
||||
}
|
||||
if m := reMax.FindStringSubmatch(content); len(m) == 2 {
|
||||
maxSize = m[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// setJVMHeap 替换 jvm.options 中的堆内存配置
|
||||
func (s *App) setJVMHeap(content string, initSize, maxSize string) string {
|
||||
if initSize != "" {
|
||||
re := regexp.MustCompile(`(?m)^-Xms\S+`)
|
||||
if re.MatchString(content) {
|
||||
content = re.ReplaceAllString(content, "-Xms"+initSize)
|
||||
} else {
|
||||
content += "\n-Xms" + initSize
|
||||
}
|
||||
}
|
||||
if maxSize != "" {
|
||||
re := regexp.MustCompile(`(?m)^-Xmx\S+`)
|
||||
if re.MatchString(content) {
|
||||
content = re.ReplaceAllString(content, "-Xmx"+maxSize)
|
||||
} else {
|
||||
content += "\n-Xmx" + maxSize
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package elasticsearch
|
||||
|
||||
// ConfigTune ElasticSearch 配置调整
|
||||
type ConfigTune struct {
|
||||
// 集群
|
||||
ClusterName string `form:"cluster_name" json:"cluster_name"`
|
||||
NodeName string `form:"node_name" json:"node_name"`
|
||||
NetworkHost string `form:"network_host" json:"network_host"`
|
||||
HTTPPort string `form:"http_port" json:"http_port"`
|
||||
DiscoveryType string `form:"discovery_type" json:"discovery_type"`
|
||||
// 路径
|
||||
PathData string `form:"path_data" json:"path_data"`
|
||||
PathLogs string `form:"path_logs" json:"path_logs"`
|
||||
// JVM
|
||||
HeapInitSize string `form:"heap_init_size" json:"heap_init_size"`
|
||||
HeapMaxSize string `form:"heap_max_size" json:"heap_max_size"`
|
||||
}
|
||||
+143
-197
@@ -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/internal/app"
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"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"`
|
||||
}
|
||||
|
||||
+314
-17
@@ -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/internal/app"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"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"`
|
||||
}
|
||||
|
||||
+11
-24
@@ -1,15 +1,14 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"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"`
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
package grafana
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"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"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
// 数据源管理
|
||||
r.Get("/datasources", s.DataSourceList)
|
||||
r.Post("/datasources", s.CreateDataSource)
|
||||
r.Post("/datasources/{name}", s.UpdateDataSource)
|
||||
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 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get grafana status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
// 从 defaults.ini 获取端口
|
||||
config, _ := io.Read(s.configPath())
|
||||
port := confval.SectionINI.GetIn(config, "server", "http_port")
|
||||
if port == "" {
|
||||
port = "3000"
|
||||
}
|
||||
|
||||
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.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var health struct {
|
||||
Commit string `json:"commit"`
|
||||
Database string `json:"database"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.Bytes(), &health); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Version"), Value: health.Version},
|
||||
{Name: "Commit", Value: health.Commit},
|
||||
{Name: s.t.Get("Database"), Value: health.Database},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.SaveConfig(w, r, s.configPath(), "grafana")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Grafana 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
get := func(section, key string) string {
|
||||
return confval.SectionINI.GetIn(config, section, key)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
// [server]
|
||||
HTTPPort: get("server", "http_port"),
|
||||
Domain: get("server", "domain"),
|
||||
RootURL: get("server", "root_url"),
|
||||
Protocol: get("server", "protocol"),
|
||||
// [database]
|
||||
DBType: get("database", "type"),
|
||||
DBHost: get("database", "host"),
|
||||
DBName: get("database", "name"),
|
||||
DBUser: get("database", "user"),
|
||||
DBPassword: get("database", "password"),
|
||||
// [security]
|
||||
AdminUser: get("security", "admin_user"),
|
||||
AdminPassword: get("security", "admin_password"),
|
||||
// [users]
|
||||
AllowSignUp: get("users", "allow_sign_up"),
|
||||
AutoAssignOrgRole: get("users", "auto_assign_org_role"),
|
||||
// [smtp]
|
||||
SMTPEnabled: get("smtp", "enabled"),
|
||||
SMTPHost: get("smtp", "host"),
|
||||
SMTPUser: get("smtp", "user"),
|
||||
SMTPPassword: get("smtp", "password"),
|
||||
SMTPFromAddress: get("smtp", "from_address"),
|
||||
// [log]
|
||||
LogMode: get("log", "mode"),
|
||||
LogLevel: get("log", "level"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Grafana 配置调整参数
|
||||
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
|
||||
}
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
// [server]
|
||||
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 = 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 = confval.SectionINI.SetIn(config, "security", "admin_user", req.AdminUser)
|
||||
config = confval.SectionINI.SetIn(config, "security", "admin_password", req.AdminPassword)
|
||||
// [users]
|
||||
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 = 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 = 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)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("grafana"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// DataSourceList 获取数据源列表
|
||||
func (s *App) DataSourceList(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, s.getDatasourceList(s.readDatasources()))
|
||||
}
|
||||
|
||||
// CreateDataSource 创建数据源
|
||||
func (s *App) CreateDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[DataSource](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := s.readDatasources()
|
||||
|
||||
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 {
|
||||
s.clearDefault(cfg)
|
||||
}
|
||||
|
||||
list := s.getDatasourceList(cfg)
|
||||
list = append(list, s.buildDatasourceMap(req))
|
||||
cfg["datasources"] = list
|
||||
|
||||
if err = s.writeDatasources(cfg); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdateDataSource 更新数据源
|
||||
func (s *App) UpdateDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
oldName := chi.URLParam(r, "name")
|
||||
req, err := service.Bind[DataSource](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := s.readDatasources()
|
||||
list := s.getDatasourceList(cfg)
|
||||
|
||||
_, 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)
|
||||
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 {
|
||||
s.addDeleteEntry(cfg, oldName)
|
||||
}
|
||||
|
||||
cfg["datasources"] = list
|
||||
|
||||
if err = s.writeDatasources(cfg); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// DeleteDataSource 删除数据源
|
||||
func (s *App) DeleteDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
cfg := s.readDatasources()
|
||||
list := s.getDatasourceList(cfg)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
cfg["datasources"] = newList
|
||||
s.addDeleteEntry(cfg, name)
|
||||
|
||||
if err := s.writeDatasources(cfg); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// configPath 返回 Grafana 主配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return app.Root + "/server/grafana/conf/defaults.ini"
|
||||
}
|
||||
|
||||
// datasourcePath 返回 provisioning 数据源文件路径
|
||||
func (s *App) datasourcePath() string {
|
||||
return app.Root + "/server/grafana/conf/provisioning/datasources/panel.yml"
|
||||
}
|
||||
|
||||
// readDatasources 读取 provisioning 文件
|
||||
func (s *App) readDatasources() map[string]any {
|
||||
raw, _ := io.Read(s.datasourcePath())
|
||||
if raw == "" {
|
||||
return map[string]any{
|
||||
"apiVersion": 1,
|
||||
"datasources": []any{},
|
||||
}
|
||||
}
|
||||
var cfg map[string]any
|
||||
if err := yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
return map[string]any{
|
||||
"apiVersion": 1,
|
||||
"datasources": []any{},
|
||||
}
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// writeDatasources 写入 provisioning 文件并重启 Grafana
|
||||
func (s *App) writeDatasources(cfg map[string]any) error {
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(s.datasourcePath(), string(data), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
return systemctl.Restart("grafana")
|
||||
}
|
||||
|
||||
// getDatasourceList 从 cfg 中提取 datasources 切片
|
||||
func (s *App) getDatasourceList(cfg map[string]any) []any {
|
||||
ds, ok := cfg["datasources"].([]any)
|
||||
if !ok {
|
||||
return []any{}
|
||||
}
|
||||
return ds
|
||||
}
|
||||
|
||||
// buildDatasourceMap 从请求构建单条 datasource map
|
||||
func (s *App) buildDatasourceMap(req *DataSource) map[string]any {
|
||||
ds := map[string]any{
|
||||
"name": req.Name,
|
||||
"type": req.Type,
|
||||
"access": req.Access,
|
||||
"url": req.URL,
|
||||
"isDefault": req.IsDefault,
|
||||
"editable": true,
|
||||
}
|
||||
if req.Access == "" {
|
||||
ds["access"] = "proxy"
|
||||
}
|
||||
switch req.Type {
|
||||
case "mysql", "postgres", "influxdb", "mssql":
|
||||
if req.Database != "" {
|
||||
ds["database"] = req.Database
|
||||
}
|
||||
if req.User != "" {
|
||||
ds["user"] = req.User
|
||||
}
|
||||
if req.Password != "" {
|
||||
ds["secureJsonData"] = map[string]any{"password": req.Password}
|
||||
}
|
||||
}
|
||||
return ds
|
||||
}
|
||||
|
||||
// clearDefault 清除所有数据源的默认标记
|
||||
func (s *App) clearDefault(cfg map[string]any) {
|
||||
lo.ForEach(s.getDatasourceList(cfg), func(item any, _ int) {
|
||||
if ds, ok := item.(map[string]any); ok {
|
||||
ds["isDefault"] = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// addDeleteEntry 向 deleteDatasources 添加条目
|
||||
func (s *App) addDeleteEntry(cfg map[string]any, name string) {
|
||||
delList, _ := cfg["deleteDatasources"].([]any)
|
||||
delList = append(delList, map[string]any{"name": name, "orgId": 1})
|
||||
cfg["deleteDatasources"] = delList
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package grafana
|
||||
|
||||
// ConfigTune Grafana 配置调整
|
||||
type ConfigTune struct {
|
||||
// [server]
|
||||
HTTPPort string `form:"http_port" json:"http_port"`
|
||||
Domain string `form:"domain" json:"domain"`
|
||||
RootURL string `form:"root_url" json:"root_url"`
|
||||
Protocol string `form:"protocol" json:"protocol"`
|
||||
// [database]
|
||||
DBType string `form:"db_type" json:"db_type"`
|
||||
DBHost string `form:"db_host" json:"db_host"`
|
||||
DBName string `form:"db_name" json:"db_name"`
|
||||
DBUser string `form:"db_user" json:"db_user"`
|
||||
DBPassword string `form:"db_password" json:"db_password"`
|
||||
// [security]
|
||||
AdminUser string `form:"admin_user" json:"admin_user"`
|
||||
AdminPassword string `form:"admin_password" json:"admin_password"`
|
||||
// [users]
|
||||
AllowSignUp string `form:"allow_sign_up" json:"allow_sign_up"`
|
||||
AutoAssignOrgRole string `form:"auto_assign_org_role" json:"auto_assign_org_role"`
|
||||
// [smtp]
|
||||
SMTPEnabled string `form:"smtp_enabled" json:"smtp_enabled"`
|
||||
SMTPHost string `form:"smtp_host" json:"smtp_host"`
|
||||
SMTPUser string `form:"smtp_user" json:"smtp_user"`
|
||||
SMTPPassword string `form:"smtp_password" json:"smtp_password"`
|
||||
SMTPFromAddress string `form:"smtp_from_address" json:"smtp_from_address"`
|
||||
// [log]
|
||||
LogMode string `form:"log_mode" json:"log_mode"`
|
||||
LogLevel string `form:"log_level" json:"log_level"`
|
||||
}
|
||||
|
||||
// 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,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" validate:"in:proxy,direct"`
|
||||
IsDefault bool `form:"is_default" json:"is_default"`
|
||||
// 数据库类型专用
|
||||
Database string `form:"database" json:"database"`
|
||||
User string `form:"user" json:"user"`
|
||||
Password string `form:"password" json:"password"`
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package kafka
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"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"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
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 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get kafka status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
data := []types.NV{
|
||||
{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) {
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.SaveConfig(w, r, s.configPath(), "kafka")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Kafka 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
heapRaw, _ := io.Read(s.heapEnvPath())
|
||||
heapInit, heapMax := s.parseHeapEnv(heapRaw)
|
||||
|
||||
tune := ConfigTune{
|
||||
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,
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Kafka 配置调整参数
|
||||
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
|
||||
}
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 JVM 堆内存
|
||||
if req.HeapInitSize != "" || req.HeapMaxSize != "" {
|
||||
heapRaw, _ := io.Read(s.heapEnvPath())
|
||||
heapRaw = s.setHeapEnv(heapRaw, req.HeapInitSize, req.HeapMaxSize)
|
||||
if err = io.Write(s.heapEnvPath(), heapRaw, 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)
|
||||
}
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return app.Root + "/server/kafka/config/server.properties"
|
||||
}
|
||||
|
||||
// heapEnvPath 返回 JVM 堆内存配置文件路径
|
||||
func (s *App) heapEnvPath() string {
|
||||
return app.Root + "/server/kafka/config/heap.env"
|
||||
}
|
||||
|
||||
// parseHeapEnv 从 heap.env 中提取堆内存配置
|
||||
func (s *App) parseHeapEnv(content string) (initSize, maxSize string) {
|
||||
re := regexp.MustCompile(`KAFKA_HEAP_OPTS=(.+)`)
|
||||
m := re.FindStringSubmatch(content)
|
||||
if len(m) != 2 {
|
||||
return
|
||||
}
|
||||
opts := m[1]
|
||||
if mi := regexp.MustCompile(`-Xms(\S+)`).FindStringSubmatch(opts); len(mi) == 2 {
|
||||
initSize = mi[1]
|
||||
}
|
||||
if mx := regexp.MustCompile(`-Xmx(\S+)`).FindStringSubmatch(opts); len(mx) == 2 {
|
||||
maxSize = mx[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// setHeapEnv 设置 heap.env 中的堆内存配置
|
||||
func (s *App) setHeapEnv(content string, initSize, maxSize string) string {
|
||||
// 读取已有值作为默认
|
||||
oldInit, oldMax := s.parseHeapEnv(content)
|
||||
if initSize == "" {
|
||||
initSize = oldInit
|
||||
}
|
||||
if maxSize == "" {
|
||||
maxSize = oldMax
|
||||
}
|
||||
if initSize == "" {
|
||||
initSize = "1g"
|
||||
}
|
||||
if maxSize == "" {
|
||||
maxSize = "1g"
|
||||
}
|
||||
return fmt.Sprintf("KAFKA_HEAP_OPTS=-Xms%s -Xmx%s\n", initSize, maxSize)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package kafka
|
||||
|
||||
// ConfigTune Kafka 配置调整
|
||||
type ConfigTune struct {
|
||||
// Broker
|
||||
NodeID string `form:"node_id" json:"node_id"` // node.id
|
||||
Listeners string `form:"listeners" json:"listeners"` // listeners
|
||||
LogDirs string `form:"log_dirs" json:"log_dirs"` // log.dirs
|
||||
NumPartitions string `form:"num_partitions" json:"num_partitions"` // num.partitions
|
||||
RetentionHours string `form:"retention_hours" json:"retention_hours"` // log.retention.hours
|
||||
LogSegmentBytes string `form:"log_segment_bytes" json:"log_segment_bytes"` // log.segment.bytes
|
||||
// JVM
|
||||
HeapInitSize string `form:"heap_init_size" json:"heap_init_size"` // -Xms
|
||||
HeapMaxSize string `form:"heap_max_size" json:"heap_max_size"` // -Xmx
|
||||
}
|
||||
@@ -2,22 +2,24 @@ package mariadb
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
)
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
"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 {
|
||||
@@ -29,6 +31,13 @@ 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)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("memcached")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -82,23 +91,55 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.ServeConfig(w, "/etc/systemd/system/memcached.service")
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.SaveConfig(w, r, "/etc/systemd/system/memcached.service", "memcached")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Memcached 配置调整参数
|
||||
func (s *App) GetConfigTune(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)
|
||||
tune := ConfigTune{
|
||||
Port: s.getExecStartArg(config, "-p"),
|
||||
UDPPort: s.getExecStartArg(config, "-U"),
|
||||
ListenAddress: s.getExecStartArg(config, "-l"),
|
||||
Memory: s.getExecStartArg(config, "-m"),
|
||||
MaxConnections: s.getExecStartArg(config, "-c"),
|
||||
Threads: s.getExecStartArg(config, "-t"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
// UpdateConfigTune 更新 Memcached 配置调整参数
|
||||
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
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/systemd/system/memcached.service", req.Config, 0644); err != nil {
|
||||
config, err := io.Read("/etc/systemd/system/memcached.service")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config = s.setExecStartArg(config, "-p", req.Port)
|
||||
config = s.setExecStartArg(config, "-U", req.UDPPort)
|
||||
config = s.setExecStartArg(config, "-l", req.ListenAddress)
|
||||
config = s.setExecStartArg(config, "-m", req.Memory)
|
||||
config = s.setExecStartArg(config, "-c", req.MaxConnections)
|
||||
config = s.setExecStartArg(config, "-t", req.Threads)
|
||||
|
||||
if err = io.Write("/etc/systemd/system/memcached.service", config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -110,3 +151,57 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getExecStartArg 从 systemd service 文件的 ExecStart 行中获取指定参数值
|
||||
func (s *App) getExecStartArg(content string, flag string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "ExecStart=") {
|
||||
continue
|
||||
}
|
||||
args := strings.Fields(trimmed)
|
||||
for i, arg := range args {
|
||||
if arg == flag && i+1 < len(args) {
|
||||
return args[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setExecStartArg 在 systemd service 文件的 ExecStart 行中设置指定参数值
|
||||
func (s *App) setExecStartArg(content string, flag 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))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "ExecStart=") {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
args := strings.Fields(trimmed)
|
||||
newArgs := make([]string, 0, len(args))
|
||||
found := false
|
||||
for i := 0; i < len(args); i++ {
|
||||
if args[i] == flag && i+1 < len(args) {
|
||||
i++ // 跳过旧值
|
||||
found = true
|
||||
// 值为空时删除该参数
|
||||
if value != "" {
|
||||
newArgs = append(newArgs, flag, value)
|
||||
}
|
||||
} else {
|
||||
newArgs = append(newArgs, args[i])
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
newArgs = append(newArgs, flag, value)
|
||||
}
|
||||
result = append(result, strings.Join(newArgs, " "))
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
@@ -1,5 +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" 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" 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"`
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"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)
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
package mongodb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"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"
|
||||
"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
|
||||
settingRepo biz.SettingRepo
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
r.Get("/admin_password", s.GetAdminPassword)
|
||||
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")
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
password, _ := s.settingRepo.Get(biz.SettingKeyMongoDBAdminPassword)
|
||||
raw, err := shell.Execf(`mongosh --quiet --eval "JSON.stringify(db.serverStatus())" -u admin -p '%s' --authenticationDatabase admin 2>/dev/null`, password)
|
||||
if err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var status2 struct {
|
||||
Uptime int `json:"uptime"`
|
||||
Connections struct {
|
||||
Current int `json:"current"`
|
||||
TotalCreated int `json:"totalCreated"`
|
||||
} `json:"connections"`
|
||||
OpCounters struct {
|
||||
Query int `json:"query"`
|
||||
Insert int `json:"insert"`
|
||||
Update int `json:"update"`
|
||||
Delete int `json:"delete"`
|
||||
} `json:"opcounters"`
|
||||
Mem struct {
|
||||
Resident int `json:"resident"`
|
||||
} `json:"mem"`
|
||||
StorageEngine struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"storageEngine"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err = json.Unmarshal([]byte(raw), &status2); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Version"), Value: status2.Version},
|
||||
{Name: s.t.Get("Uptime (seconds)"), Value: cast.ToString(status2.Uptime)},
|
||||
{Name: s.t.Get("Current Connections"), Value: cast.ToString(status2.Connections.Current)},
|
||||
{Name: s.t.Get("Total Connections Created"), Value: cast.ToString(status2.Connections.TotalCreated)},
|
||||
{Name: s.t.Get("Query Operations"), Value: cast.ToString(status2.OpCounters.Query)},
|
||||
{Name: s.t.Get("Insert Operations"), Value: cast.ToString(status2.OpCounters.Insert)},
|
||||
{Name: s.t.Get("Update Operations"), Value: cast.ToString(status2.OpCounters.Update)},
|
||||
{Name: s.t.Get("Delete Operations"), Value: cast.ToString(status2.OpCounters.Delete)},
|
||||
{Name: s.t.Get("Resident Memory (MB)"), Value: cast.ToString(status2.Mem.Resident)},
|
||||
{Name: s.t.Get("Storage Engine"), Value: status2.StorageEngine.Name},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.SaveConfig(w, r, s.configPath(), "mongod")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg == nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
DbPath: s.getYAMLValue(cfg, "storage.dbPath"),
|
||||
CacheSizeGB: s.getYAMLValue(cfg, "storage.wiredTiger.engineConfig.cacheSizeGB"),
|
||||
Port: s.getYAMLValue(cfg, "net.port"),
|
||||
BindIp: s.getYAMLValue(cfg, "net.bindIp"),
|
||||
SystemLogPath: s.getYAMLValue(cfg, "systemLog.path"),
|
||||
Authorization: s.getYAMLValue(cfg, "security.authorization"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新配置调整参数
|
||||
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
|
||||
}
|
||||
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
s.setNestedYAMLValue(cfg, "storage.dbPath", req.DbPath)
|
||||
s.setNestedYAMLValue(cfg, "storage.wiredTiger.engineConfig.cacheSizeGB", req.CacheSizeGB)
|
||||
s.setNestedYAMLValue(cfg, "net.port", req.Port)
|
||||
s.setNestedYAMLValue(cfg, "net.bindIp", req.BindIp)
|
||||
s.setNestedYAMLValue(cfg, "systemLog.path", req.SystemLogPath)
|
||||
s.setNestedYAMLValue(cfg, "security.authorization", req.Authorization)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Write(s.configPath(), string(data), 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)
|
||||
}
|
||||
|
||||
// GetAdminPassword 获取 admin 密码
|
||||
func (s *App) GetAdminPassword(w http.ResponseWriter, r *http.Request) {
|
||||
password, err := s.settingRepo.Get(biz.SettingKeyMongoDBAdminPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get MongoDB admin password: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, password)
|
||||
}
|
||||
|
||||
// SetAdminPassword 设置 admin 密码
|
||||
func (s *App) SetAdminPassword(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[SetAdminPassword](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
oldPassword, _ := s.settingRepo.Get(biz.SettingKeyMongoDBAdminPassword)
|
||||
|
||||
// 尝试用旧密码连接修改
|
||||
_, err = shell.Execf(`mongosh --quiet -u admin -p '%s' --authenticationDatabase admin --eval "db.changeUserPassword('admin', '%s')"`, oldPassword, req.Password)
|
||||
if err != nil {
|
||||
// 回退:停止服务,无认证模式修改
|
||||
_ = systemctl.Stop("mongod")
|
||||
_, _ = shell.Execf(`su -s /bin/bash mongod -c "mongod --config %s --noauth --fork --logpath /tmp/mongod_reset.log"`, s.configPath())
|
||||
_, resetErr := shell.Execf(`mongosh --quiet --eval "db.getSiblingDB('admin').changeUserPassword('admin', '%s')"`, req.Password)
|
||||
_, _ = shell.Execf(`su -s /bin/bash mongod -c "mongod --config %s --shutdown" 2>/dev/null; pkill -f 'mongod --config.*--noauth' 2>/dev/null`, s.configPath())
|
||||
_ = systemctl.Start("mongod")
|
||||
if resetErr != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set MongoDB admin password: %v", resetErr))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.settingRepo.Set(biz.SettingKeyMongoDBAdminPassword, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to save MongoDB admin password: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_mongodb", req.Password)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return app.Root + "/server/mongodb/mongod.conf"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取嵌套 YAML 值,支持 dot notation
|
||||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||||
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])
|
||||
}
|
||||
|
||||
// setNestedYAMLValue 设置嵌套 YAML 值,逐层创建 map
|
||||
func (s *App) setNestedYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
if len(parts) == 1 {
|
||||
cfg[parts[0]] = value
|
||||
return
|
||||
}
|
||||
nested, ok := cfg[parts[0]].(map[string]any)
|
||||
if !ok {
|
||||
nested = make(map[string]any)
|
||||
cfg[parts[0]] = nested
|
||||
}
|
||||
s.setNestedYAMLValue(nested, parts[1], value)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package mongodb
|
||||
|
||||
// SetAdminPassword 设置 admin 密码
|
||||
type SetAdminPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
// ConfigTune MongoDB 配置调整
|
||||
type ConfigTune struct {
|
||||
// 存储
|
||||
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" validate:"number && min:1 && max:65535"`
|
||||
BindIp string `form:"bind_ip" json:"bind_ip"`
|
||||
// 日志
|
||||
SystemLogPath string `form:"system_log_path" json:"system_log_path" validate:"unix_path"`
|
||||
// 安全
|
||||
Authorization string `form:"authorization" json:"authorization" validate:"in:enabled,disabled"`
|
||||
}
|
||||
+693
-86
@@ -1,37 +1,43 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/db"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"github.com/acepanel/panel/pkg/tools"
|
||||
"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"
|
||||
"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
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,43 +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 获取负载
|
||||
@@ -92,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 {
|
||||
@@ -191,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密码
|
||||
@@ -236,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
|
||||
}
|
||||
@@ -261,26 +258,636 @@ 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"
|
||||
// GetConfigTune 获取 MySQL 配置调整参数
|
||||
func (s *App) GetConfigTune(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
|
||||
}
|
||||
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]
|
||||
|
||||
tune := ConfigTune{
|
||||
// 常规设置
|
||||
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: 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: 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: confval.INI.Get(config, "slow_query_log"),
|
||||
LongQueryTime: confval.INI.Get(config, "long_query_time"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 MySQL 配置调整参数
|
||||
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/mysql/conf/my.cnf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新常规设置
|
||||
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 = 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 = 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 = 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)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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 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]
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, processes)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
// KILL 不支持预编译参数,id 已校验为正整数
|
||||
if _, err = mysql.Exec(fmt.Sprintf(`KILL %d`, id)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return "/tmp/mysql.sock"
|
||||
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,5 +5,152 @@ 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" 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"`
|
||||
TableOpenCache string `form:"table_open_cache" json:"table_open_cache"`
|
||||
MaxAllowedPacket string `form:"max_allowed_packet" json:"max_allowed_packet"`
|
||||
OpenFilesLimit string `form:"open_files_limit" json:"open_files_limit"`
|
||||
// 性能调整
|
||||
KeyBufferSize string `form:"key_buffer_size" json:"key_buffer_size"`
|
||||
SortBufferSize string `form:"sort_buffer_size" json:"sort_buffer_size"`
|
||||
ReadBufferSize string `form:"read_buffer_size" json:"read_buffer_size"`
|
||||
ReadRndBufferSize string `form:"read_rnd_buffer_size" json:"read_rnd_buffer_size"`
|
||||
JoinBufferSize string `form:"join_buffer_size" json:"join_buffer_size"`
|
||||
ThreadCacheSize string `form:"thread_cache_size" json:"thread_cache_size"`
|
||||
ThreadStack string `form:"thread_stack" json:"thread_stack"`
|
||||
TmpTableSize string `form:"tmp_table_size" json:"tmp_table_size"`
|
||||
MaxHeapTableSize string `form:"max_heap_table_size" json:"max_heap_table_size"`
|
||||
MyisamSortBufferSize string `form:"myisam_sort_buffer_size" json:"myisam_sort_buffer_size"`
|
||||
// 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" 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"`
|
||||
InnodbWriteIoThreads string `form:"innodb_write_io_threads" json:"innodb_write_io_threads"`
|
||||
// 日志
|
||||
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"`
|
||||
}
|
||||
|
||||
+121
-11
@@ -7,17 +7,18 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"github.com/acepanel/panel/pkg/tools"
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
"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"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -25,6 +26,7 @@ type App struct {
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
@@ -36,6 +38,8 @@ 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)
|
||||
|
||||
r.Get("/stream/servers", s.ListStreamServers)
|
||||
r.Post("/stream/servers", s.CreateStreamServer)
|
||||
@@ -47,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
|
||||
@@ -64,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
|
||||
}
|
||||
@@ -93,8 +102,9 @@ func (s *App) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -165,3 +175,103 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Nginx 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(app.Root + "/server/nginx/conf/nginx.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
// 常规设置
|
||||
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: 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: 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: 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)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Nginx 配置调整参数
|
||||
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/nginx/conf/nginx.conf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新常规设置
|
||||
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 = 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 = 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 = 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)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload nginx: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -6,23 +6,55 @@ type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Nginx 配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
WorkerProcesses string `form:"worker_processes" json:"worker_processes"`
|
||||
WorkerConnections string `form:"worker_connections" json:"worker_connections"`
|
||||
KeepaliveTimeout string `form:"keepalive_timeout" json:"keepalive_timeout"`
|
||||
ClientMaxBodySize string `form:"client_max_body_size" json:"client_max_body_size"`
|
||||
ClientBodyBufferSize string `form:"client_body_buffer_size" json:"client_body_buffer_size"`
|
||||
ClientHeaderBufferSize string `form:"client_header_buffer_size" json:"client_header_buffer_size"`
|
||||
ServerNamesHashBucketSize string `form:"server_names_hash_bucket_size" json:"server_names_hash_bucket_size"`
|
||||
ServerTokens string `form:"server_tokens" json:"server_tokens"`
|
||||
// Gzip 压缩
|
||||
Gzip string `form:"gzip" json:"gzip"`
|
||||
GzipMinLength string `form:"gzip_min_length" json:"gzip_min_length"`
|
||||
GzipCompLevel string `form:"gzip_comp_level" json:"gzip_comp_level"`
|
||||
GzipTypes string `form:"gzip_types" json:"gzip_types"`
|
||||
GzipVary string `form:"gzip_vary" json:"gzip_vary"`
|
||||
GzipProxied string `form:"gzip_proxied" json:"gzip_proxied"`
|
||||
// Brotli 压缩
|
||||
Brotli string `form:"brotli" json:"brotli"`
|
||||
BrotliMinLength string `form:"brotli_min_length" json:"brotli_min_length"`
|
||||
BrotliCompLevel string `form:"brotli_comp_level" json:"brotli_comp_level"`
|
||||
BrotliTypes string `form:"brotli_types" json:"brotli_types"`
|
||||
BrotliStatic string `form:"brotli_static" json:"brotli_static"`
|
||||
// Zstd 压缩
|
||||
Zstd string `form:"zstd" json:"zstd"`
|
||||
ZstdMinLength string `form:"zstd_min_length" json:"zstd_min_length"`
|
||||
ZstdCompLevel string `form:"zstd_comp_level" json:"zstd_comp_level"`
|
||||
ZstdTypes string `form:"zstd_types" json:"zstd_types"`
|
||||
ZstdStatic string `form:"zstd_static" json:"zstd_static"`
|
||||
}
|
||||
|
||||
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,11 +11,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
webserverNginx "github.com/acepanel/panel/pkg/webserver/nginx"
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
webserverNginx "github.com/acepanel/panel/v3/pkg/webserver/nginx"
|
||||
)
|
||||
|
||||
// ListStreamServers 获取 Stream Server 列表
|
||||
@@ -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 块中的指令
|
||||
@@ -547,34 +549,31 @@ func (s *App) saveStreamServerConfig(filePath string, server *StreamServer) erro
|
||||
// saveStreamUpstreamConfig 生成并保存 Stream Upstream 配置
|
||||
func (s *App) saveStreamUpstreamConfig(filePath string, upstream *StreamUpstream) error {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("upstream %s {\n", upstream.Name))
|
||||
_, _ = fmt.Fprintf(&sb, "upstream %s {\n", upstream.Name)
|
||||
|
||||
// 负载均衡算法
|
||||
if upstream.Algo != "" {
|
||||
sb.WriteString(fmt.Sprintf(" %s;\n", upstream.Algo))
|
||||
_, _ = fmt.Fprintf(&sb, " %s;\n", upstream.Algo)
|
||||
}
|
||||
|
||||
// resolver 配置
|
||||
if len(upstream.Resolver) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" resolver %s;\n", strings.Join(upstream.Resolver, " ")))
|
||||
_, _ = fmt.Fprintf(&sb, " resolver %s;\n", strings.Join(upstream.Resolver, " "))
|
||||
if upstream.ResolverTimeout > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" resolver_timeout %s;\n", formatNginxDuration(upstream.ResolverTimeout)))
|
||||
_, _ = fmt.Fprintf(&sb, " resolver_timeout %s;\n", formatNginxDuration(upstream.ResolverTimeout))
|
||||
}
|
||||
}
|
||||
|
||||
// 服务器列表
|
||||
var addrs []string
|
||||
for addr := range upstream.Servers {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
addrs := lo.Keys(upstream.Servers)
|
||||
sort.Strings(addrs)
|
||||
|
||||
for _, addr := range addrs {
|
||||
options := upstream.Servers[addr]
|
||||
if options != "" {
|
||||
sb.WriteString(fmt.Sprintf(" server %s %s;\n", addr, options))
|
||||
_, _ = fmt.Fprintf(&sb, " server %s %s;\n", addr, options)
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" server %s;\n", addr))
|
||||
_, _ = fmt.Fprintf(&sb, " server %s;\n", addr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,21 +2,27 @@ package openresty
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/internal/apps/nginx"
|
||||
"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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
package opensearch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"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/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 {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
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 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get opensearch status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
port := s.getPort()
|
||||
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.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var health struct {
|
||||
ClusterName string `json:"cluster_name"`
|
||||
Status string `json:"status"`
|
||||
NumberOfNodes int `json:"number_of_nodes"`
|
||||
NumberOfDataNodes int `json:"number_of_data_nodes"`
|
||||
ActiveShards int `json:"active_shards"`
|
||||
ActivePrimaryShards int `json:"active_primary_shards"`
|
||||
RelocatingShards int `json:"relocating_shards"`
|
||||
UnassignedShards int `json:"unassigned_shards"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.Bytes(), &health); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Cluster Name"), Value: health.ClusterName},
|
||||
{Name: s.t.Get("Cluster Status"), Value: health.Status},
|
||||
{Name: s.t.Get("Number of Nodes"), Value: cast.ToString(health.NumberOfNodes)},
|
||||
{Name: s.t.Get("Number of Data Nodes"), Value: cast.ToString(health.NumberOfDataNodes)},
|
||||
{Name: s.t.Get("Active Shards"), Value: cast.ToString(health.ActiveShards)},
|
||||
{Name: s.t.Get("Active Primary Shards"), Value: cast.ToString(health.ActivePrimaryShards)},
|
||||
{Name: s.t.Get("Relocating Shards"), Value: cast.ToString(health.RelocatingShards)},
|
||||
{Name: s.t.Get("Unassigned Shards"), Value: cast.ToString(health.UnassignedShards)},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.SaveConfig(w, r, s.configPath(), "opensearch")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 OpenSearch 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg == nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
jvmRaw, _ := io.Read(s.jvmOptionsPath())
|
||||
heapInit, heapMax := s.parseJVMHeap(jvmRaw)
|
||||
|
||||
tune := ConfigTune{
|
||||
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,
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 OpenSearch 配置调整参数
|
||||
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
|
||||
}
|
||||
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
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 {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Write(s.configPath(), string(data), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if req.HeapInitSize != "" || req.HeapMaxSize != "" {
|
||||
jvmRaw, _ := io.Read(s.jvmOptionsPath())
|
||||
jvmRaw = s.setJVMHeap(jvmRaw, req.HeapInitSize, req.HeapMaxSize)
|
||||
if err = io.Write(s.jvmOptionsPath(), jvmRaw, 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)
|
||||
}
|
||||
|
||||
func (s *App) configPath() string {
|
||||
return app.Root + "/server/opensearch/config/opensearch.yml"
|
||||
}
|
||||
|
||||
func (s *App) jvmOptionsPath() string {
|
||||
return app.Root + "/server/opensearch/config/jvm.options"
|
||||
}
|
||||
|
||||
func (s *App) getPort() string {
|
||||
raw, _ := io.Read(s.configPath())
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := confval.GetYAML(cfg, "http.port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "9200"
|
||||
}
|
||||
|
||||
func (s *App) parseJVMHeap(content string) (initSize, maxSize string) {
|
||||
reInit := regexp.MustCompile(`(?m)^-Xms(\S+)`)
|
||||
reMax := regexp.MustCompile(`(?m)^-Xmx(\S+)`)
|
||||
if m := reInit.FindStringSubmatch(content); len(m) == 2 {
|
||||
initSize = m[1]
|
||||
}
|
||||
if m := reMax.FindStringSubmatch(content); len(m) == 2 {
|
||||
maxSize = m[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *App) setJVMHeap(content string, initSize, maxSize string) string {
|
||||
if initSize != "" {
|
||||
re := regexp.MustCompile(`(?m)^-Xms\S+`)
|
||||
if re.MatchString(content) {
|
||||
content = re.ReplaceAllString(content, "-Xms"+initSize)
|
||||
} else {
|
||||
content += "\n-Xms" + initSize
|
||||
}
|
||||
}
|
||||
if maxSize != "" {
|
||||
re := regexp.MustCompile(`(?m)^-Xmx\S+`)
|
||||
if re.MatchString(content) {
|
||||
content = re.ReplaceAllString(content, "-Xmx"+maxSize)
|
||||
} else {
|
||||
content += "\n-Xmx" + maxSize
|
||||
}
|
||||
}
|
||||
return content
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package opensearch
|
||||
|
||||
// ConfigTune OpenSearch 配置调整
|
||||
type ConfigTune struct {
|
||||
// 集群
|
||||
ClusterName string `form:"cluster_name" json:"cluster_name"`
|
||||
NodeName string `form:"node_name" json:"node_name"`
|
||||
NetworkHost string `form:"network_host" json:"network_host"`
|
||||
HTTPPort string `form:"http_port" json:"http_port"`
|
||||
DiscoveryType string `form:"discovery_type" json:"discovery_type"`
|
||||
// 路径
|
||||
PathData string `form:"path_data" json:"path_data"`
|
||||
PathLogs string `form:"path_logs" json:"path_logs"`
|
||||
// JVM
|
||||
HeapInitSize string `form:"heap_init_size" json:"heap_init_size"`
|
||||
HeapMaxSize string `form:"heap_max_size" json:"heap_max_size"`
|
||||
}
|
||||
@@ -2,22 +2,24 @@ package percona
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/internal/apps/mysql"
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mysql"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
+196
-27
@@ -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/internal/app"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/firewall"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"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
|
||||
}
|
||||
@@ -95,8 +264,8 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Direction: firewall.DirectionIn,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
Direction: firewall.DirectionIn,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"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 {
|
||||
|
||||
+724
-44
@@ -1,35 +1,49 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/acepanel/panel/pkg/db"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
"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"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,15 +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
|
||||
@@ -65,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
|
||||
}
|
||||
|
||||
@@ -81,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
|
||||
@@ -98,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
|
||||
}
|
||||
@@ -125,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
|
||||
@@ -145,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},
|
||||
@@ -172,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 用户密码
|
||||
@@ -207,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
|
||||
}
|
||||
@@ -231,3 +258,656 @@ func (s *App) SetPostgresPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 PostgreSQL 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(s.configPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
// 连接设置
|
||||
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: 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: 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: confval.Postgres.Get(config, "default_statistics_target"),
|
||||
RandomPageCost: confval.Postgres.Get(config, "random_page_cost"),
|
||||
EffectiveIoConcurrency: confval.Postgres.Get(config, "effective_io_concurrency"),
|
||||
// 日志设置
|
||||
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: confval.Postgres.Get(config, "io_method"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 PostgreSQL 配置调整参数
|
||||
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 := s.configPath()
|
||||
oldPort := db.PostgresPort(app.Root)
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新连接设置
|
||||
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 = 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 = 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 = 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 = 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 = 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 = 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)
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.Success(w, extensions)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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 -- '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
|
||||
}
|
||||
|
||||
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,5 +5,161 @@ 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" validate:"number && min:1 && max:65535"`
|
||||
MaxConnections string `form:"max_connections" json:"max_connections"`
|
||||
SuperuserReservedConnections string `form:"superuser_reserved_connections" json:"superuser_reserved_connections"`
|
||||
// 内存设置
|
||||
SharedBuffers string `form:"shared_buffers" json:"shared_buffers"`
|
||||
WorkMem string `form:"work_mem" json:"work_mem"`
|
||||
MaintenanceWorkMem string `form:"maintenance_work_mem" json:"maintenance_work_mem"`
|
||||
EffectiveCacheSize string `form:"effective_cache_size" json:"effective_cache_size"`
|
||||
HugePages string `form:"huge_pages" json:"huge_pages"`
|
||||
// WAL 设置
|
||||
WalLevel string `form:"wal_level" json:"wal_level"`
|
||||
WalBuffers string `form:"wal_buffers" json:"wal_buffers"`
|
||||
MaxWalSize string `form:"max_wal_size" json:"max_wal_size"`
|
||||
MinWalSize string `form:"min_wal_size" json:"min_wal_size"`
|
||||
CheckpointCompletionTarget string `form:"checkpoint_completion_target" json:"checkpoint_completion_target"`
|
||||
// 查询优化
|
||||
DefaultStatisticsTarget string `form:"default_statistics_target" json:"default_statistics_target"`
|
||||
RandomPageCost string `form:"random_page_cost" json:"random_page_cost"`
|
||||
EffectiveIoConcurrency string `form:"effective_io_concurrency" json:"effective_io_concurrency"`
|
||||
// 日志设置
|
||||
LogDestination string `form:"log_destination" json:"log_destination"`
|
||||
LogMinDurationStatement string `form:"log_min_duration_statement" json:"log_min_duration_statement"`
|
||||
LogTimezone string `form:"log_timezone" json:"log_timezone"`
|
||||
// IO 设置
|
||||
IoMethod string `form:"io_method" json:"io_method"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,420 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
conf *config.Config
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, taskRepo biz.TaskRepo) *App {
|
||||
|
||||
return &App{t: t, conf: conf, taskRepo: 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)
|
||||
// Alertmanager 配置
|
||||
r.Get("/alertmanager_config", s.GetAlertmanagerConfig)
|
||||
r.Post("/alertmanager_config", s.UpdateAlertmanagerConfig)
|
||||
// Exporters 管理
|
||||
r.Get("/exporters", s.ExporterList)
|
||||
r.Post("/exporters", s.InstallExporter)
|
||||
r.Delete("/exporters", s.UninstallExporter)
|
||||
r.Post("/exporters/{slug}/start", s.StartExporter)
|
||||
r.Post("/exporters/{slug}/stop", s.StopExporter)
|
||||
r.Post("/exporters/{slug}/restart", s.RestartExporter)
|
||||
r.Get("/exporters/{slug}/config", s.GetExporterConfig)
|
||||
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 {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get prometheus status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
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.IsStatusSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Data struct {
|
||||
StartTime string `json:"startTime"`
|
||||
GoroutineCount int `json:"goroutineCount"`
|
||||
GOMAXPROCS int `json:"GOMAXPROCS"`
|
||||
StorageRetention string `json:"storageRetention"`
|
||||
ReloadConfigSuccess bool `json:"reloadConfigSuccess"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.Bytes(), &result); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Start Time"), Value: result.Data.StartTime},
|
||||
{Name: s.t.Get("Storage Retention"), Value: result.Data.StorageRetention},
|
||||
{Name: s.t.Get("Goroutine Count"), Value: cast.ToString(result.Data.GoroutineCount)},
|
||||
{Name: "GOMAXPROCS", Value: cast.ToString(result.Data.GOMAXPROCS)},
|
||||
{Name: s.t.Get("Config Reload Success"), Value: cast.ToString(result.Data.ReloadConfigSuccess)},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.ServeConfig(w, app.Root+"/server/prometheus/prometheus.yml")
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
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(app.Root + "/server/prometheus/prometheus.yml")
|
||||
|
||||
var cfg struct {
|
||||
Global struct {
|
||||
ScrapeInterval string `yaml:"scrape_interval"`
|
||||
EvaluationInterval string `yaml:"evaluation_interval"`
|
||||
ScrapeTimeout string `yaml:"scrape_timeout"`
|
||||
} `yaml:"global"`
|
||||
}
|
||||
_ = yaml.Unmarshal([]byte(conf), &cfg)
|
||||
|
||||
tune := ConfigTune{
|
||||
ScrapeInterval: cfg.Global.ScrapeInterval,
|
||||
EvaluationInterval: cfg.Global.EvaluationInterval,
|
||||
ScrapeTimeout: cfg.Global.ScrapeTimeout,
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Prometheus 全局配置调整参数
|
||||
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/prometheus/prometheus.yml"
|
||||
raw, _ := io.Read(confPath)
|
||||
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
global, ok := cfg["global"].(map[string]any)
|
||||
if !ok {
|
||||
global = make(map[string]any)
|
||||
}
|
||||
|
||||
if req.ScrapeInterval != "" {
|
||||
global["scrape_interval"] = req.ScrapeInterval
|
||||
}
|
||||
if req.EvaluationInterval != "" {
|
||||
global["evaluation_interval"] = req.EvaluationInterval
|
||||
}
|
||||
if req.ScrapeTimeout != "" {
|
||||
global["scrape_timeout"] = req.ScrapeTimeout
|
||||
}
|
||||
cfg["global"] = global
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(confPath, string(data), 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)
|
||||
}
|
||||
|
||||
// GetAlertmanagerConfig 获取 Alertmanager 配置
|
||||
func (s *App) GetAlertmanagerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(app.Root + "/server/prometheus/alertmanager/alertmanager.yml")
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
// UpdateAlertmanagerConfig 更新 Alertmanager 配置
|
||||
func (s *App) UpdateAlertmanagerConfig(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/prometheus/alertmanager/alertmanager.yml", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("alertmanager"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ExporterList 获取 Exporter 列表及状态
|
||||
func (s *App) ExporterList(w http.ResponseWriter, r *http.Request) {
|
||||
exporters := s.getExporters()
|
||||
for i := range exporters {
|
||||
exporters[i].Installed = io.Exists(fmt.Sprintf("%s/server/prometheus/exporters/%s", app.Root, exporters[i].Slug))
|
||||
if exporters[i].Installed {
|
||||
running, _ := systemctl.Status("prometheus-" + exporters[i].Slug)
|
||||
exporters[i].Running = running
|
||||
}
|
||||
}
|
||||
|
||||
service.Success(w, exporters)
|
||||
}
|
||||
|
||||
// InstallExporter 安装 Exporter(异步任务)
|
||||
func (s *App) InstallExporter(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExporterSlug](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.checkExporter(req.Slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UninstallExporter 卸载 Exporter(异步任务)
|
||||
func (s *App) UninstallExporter(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExporterSlug](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.checkExporter(req.Slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// StartExporter 启动 Exporter
|
||||
func (s *App) StartExporter(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Start("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// StopExporter 停止 Exporter
|
||||
func (s *App) StopExporter(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Stop("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// RestartExporter 重启 Exporter
|
||||
func (s *App) RestartExporter(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Restart("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetExporterConfig 获取 Exporter 配置
|
||||
func (s *App) GetExporterConfig(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
confPath := s.getExporterConfigPath(slug)
|
||||
if confPath == "" {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s has no configuration file", slug))
|
||||
return
|
||||
}
|
||||
|
||||
conf, _ := io.Read(confPath)
|
||||
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
// UpdateExporterConfig 更新 Exporter 配置
|
||||
func (s *App) UpdateExporterConfig(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
confPath := s.getExporterConfigPath(slug)
|
||||
if confPath == "" {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s has no configuration file", slug))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := service.Bind[ExporterConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(confPath, req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getExporters 返回所有 exporter 定义
|
||||
func (s *App) getExporters() []Exporter {
|
||||
return []Exporter{
|
||||
{Name: "Node Exporter", Slug: "node_exporter", Description: s.t.Get("Hardware and OS metrics")},
|
||||
{Name: "Nginx Exporter", Slug: "nginx_exporter", Description: s.t.Get("Nginx metrics")},
|
||||
{Name: "MySQL Exporter", Slug: "mysqld_exporter", Description: s.t.Get("MySQL database metrics"), HasConfig: true},
|
||||
{Name: "PostgreSQL Exporter", Slug: "postgres_exporter", Description: s.t.Get("PostgreSQL database metrics"), HasConfig: true},
|
||||
{Name: "MongoDB Exporter", Slug: "mongodb_exporter", Description: s.t.Get("MongoDB metrics"), HasConfig: true},
|
||||
{Name: "Elasticsearch Exporter", Slug: "elasticsearch_exporter", Description: s.t.Get("Elasticsearch metrics"), HasConfig: true},
|
||||
{Name: "Redis Exporter", Slug: "redis_exporter", Description: s.t.Get("Redis metrics"), HasConfig: true},
|
||||
{Name: "Memcached Exporter", Slug: "memcached_exporter", Description: s.t.Get("Memcached metrics")},
|
||||
{Name: "Kafka Exporter", Slug: "kafka_exporter", Description: s.t.Get("Kafka metrics"), HasConfig: true},
|
||||
}
|
||||
}
|
||||
|
||||
// getExporterConfigPath 获取 exporter 配置文件路径
|
||||
func (s *App) getExporterConfigPath(slug string) string {
|
||||
base := fmt.Sprintf("%s/server/prometheus/exporters/%s", app.Root, slug)
|
||||
switch slug {
|
||||
case "redis_exporter", "postgres_exporter", "elasticsearch_exporter", "mongodb_exporter", "kafka_exporter":
|
||||
return base + "/env"
|
||||
case "mysqld_exporter":
|
||||
return base + "/.my.cnf"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// checkExporter 检查 slug 是否有效
|
||||
func (s *App) checkExporter(slug string) bool {
|
||||
return lo.ContainsBy(s.getExporters(), func(e Exporter) bool {
|
||||
return e.Slug == slug
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package prometheus
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Prometheus 全局配置调整
|
||||
type ConfigTune struct {
|
||||
ScrapeInterval string `form:"scrape_interval" json:"scrape_interval"`
|
||||
EvaluationInterval string `form:"evaluation_interval" json:"evaluation_interval"`
|
||||
ScrapeTimeout string `form:"scrape_timeout" json:"scrape_timeout"`
|
||||
}
|
||||
|
||||
// Exporter Prometheus Exporter 信息
|
||||
type Exporter struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
Running bool `json:"running"`
|
||||
HasConfig bool `json:"has_config"`
|
||||
}
|
||||
|
||||
// ExporterSlug Exporter 操作请求
|
||||
type ExporterSlug struct {
|
||||
Slug string `form:"slug" json:"slug" validate:"required"`
|
||||
}
|
||||
|
||||
// ExporterConfig Exporter 配置更新请求
|
||||
type ExporterConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
+102
-20
@@ -1,21 +1,25 @@
|
||||
package pureftpd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"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/internal/app"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/firewall"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"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 {
|
||||
@@ -23,6 +27,7 @@ type App struct {
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
@@ -35,6 +40,13 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/users/{username}/password", s.ChangePassword)
|
||||
r.Get("/port", s.GetPort)
|
||||
r.Post("/port", s.UpdatePort)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("pure-ftpd")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
@@ -47,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)
|
||||
|
||||
@@ -139,13 +149,19 @@ func (s *App) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetPort 获取端口
|
||||
func (s *App) GetPort(w http.ResponseWriter, r *http.Request) {
|
||||
port, err := shell.Execf(`cat %s/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`, app.Root)
|
||||
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
|
||||
}
|
||||
|
||||
service.Success(w, cast.ToInt(port))
|
||||
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]))
|
||||
}
|
||||
|
||||
service.Success(w, port)
|
||||
}
|
||||
|
||||
// UpdatePort 设置端口
|
||||
@@ -156,7 +172,14 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf(`sed -i "s/Bind.*/Bind 0.0.0.0,%d/g" %s/server/pure-ftpd/etc/pure-ftpd.conf`, req.Port, app.Root); err != nil {
|
||||
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 = 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
|
||||
}
|
||||
@@ -166,8 +189,8 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Direction: firewall.DirectionIn,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
Direction: firewall.DirectionIn,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -181,3 +204,62 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Pure-FTPd 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
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: 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)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Pure-FTPd 配置调整参数
|
||||
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/pure-ftpd/etc/pure-ftpd.conf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("pure-ftpd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -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,9 +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" 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" 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"`
|
||||
}
|
||||
|
||||
+362
-29
@@ -1,29 +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/internal/app"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
"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
|
||||
t *gotext.Locale
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
taskRepo biz.TaskRepo
|
||||
slug string // 服务名与配置目录名,如 redis、valkey
|
||||
name string // 展示名,如 Redis、Valkey
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *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,
|
||||
t: t,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
taskRepo: taskRepo,
|
||||
slug: slug,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,12 +54,26 @@ 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)
|
||||
// 性能诊断
|
||||
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 {
|
||||
@@ -44,34 +81,32 @@ 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
|
||||
}
|
||||
re := regexp.MustCompile(`^requirepass\s+(.+)`)
|
||||
re := regexp.MustCompile(`(?m)^requirepass\s+(.+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) == 2 {
|
||||
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"]},
|
||||
@@ -93,31 +128,329 @@ 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))
|
||||
common.ServeConfig(w, s.confPath())
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
common.SaveConfig(w, r, s.confPath(), s.slug)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(s.confPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
tune := ConfigTune{
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
// UpdateConfigTune 更新配置调整参数
|
||||
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
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/redis/redis.conf", app.Root), req.Config, 0644); err != nil {
|
||||
confPath := s.confPath()
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("redis"); err != nil {
|
||||
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(s.slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 同步密码到数据库服务器记录
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_"+s.slug, req.Requirepass)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
service.Success(w, entries)
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
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
|
||||
}
|
||||
fields := map[string]string{}
|
||||
for kv := range strings.FieldsSeq(line) {
|
||||
if key, value, found := strings.Cut(kv, "="); found {
|
||||
fields[key] = value
|
||||
}
|
||||
}
|
||||
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"],
|
||||
})
|
||||
}
|
||||
|
||||
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,5 +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 协议兼容服务的配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
Bind string `form:"bind" json:"bind"`
|
||||
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" 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" validate:"in:noeviction,allkeys-lru,allkeys-lfu,allkeys-random,volatile-lru,volatile-lfu,volatile-random,volatile-ttl"`
|
||||
// 持久化
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
package rocketmq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"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"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
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")
|
||||
|
||||
namesrvStr := "stopped"
|
||||
if namesrvStatus {
|
||||
namesrvStr = "running"
|
||||
}
|
||||
brokerStr := "stopped"
|
||||
if brokerStatus {
|
||||
brokerStr = "running"
|
||||
}
|
||||
|
||||
if !namesrvStatus && !brokerStatus {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
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: 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)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
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 = s.restartServices(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 RocketMQ 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
heapRaw, _ := io.Read(s.heapEnvPath())
|
||||
|
||||
namesrvInit, namesrvMax := s.parseHeapLine(heapRaw, "ROCKETMQ_NAMESRV_HEAP")
|
||||
brokerInit, brokerMax := s.parseHeapLine(heapRaw, "ROCKETMQ_BROKER_HEAP")
|
||||
|
||||
tune := ConfigTune{
|
||||
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,
|
||||
BrokerHeapMaxSize: brokerMax,
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 RocketMQ 配置调整参数
|
||||
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
|
||||
}
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新 JVM 堆内存
|
||||
if req.NamesrvHeapInitSize != "" || req.NamesrvHeapMaxSize != "" || req.BrokerHeapInitSize != "" || req.BrokerHeapMaxSize != "" {
|
||||
heapRaw, _ := io.Read(s.heapEnvPath())
|
||||
heapRaw = s.setHeapEnv(heapRaw, *req)
|
||||
if err = io.Write(s.heapEnvPath(), heapRaw, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.restartServices(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// restartServices 重启 NameServer 和 Broker 服务
|
||||
func (s *App) restartServices() error {
|
||||
if err := systemctl.Restart("rocketmq-namesrv"); err != nil {
|
||||
return err
|
||||
}
|
||||
return systemctl.Restart("rocketmq-broker")
|
||||
}
|
||||
|
||||
// configPath 返回 broker 配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return app.Root + "/server/rocketmq/conf/broker.conf"
|
||||
}
|
||||
|
||||
// heapEnvPath 返回 JVM 堆内存配置文件路径
|
||||
func (s *App) heapEnvPath() string {
|
||||
return app.Root + "/server/rocketmq/conf/heap.env"
|
||||
}
|
||||
|
||||
// parseHeapLine 从 heap.env 中提取指定环境变量的堆内存配置
|
||||
func (s *App) parseHeapLine(content string, envKey string) (initSize, maxSize string) {
|
||||
re := regexp.MustCompile(envKey + `=(.+)`)
|
||||
m := re.FindStringSubmatch(content)
|
||||
if len(m) != 2 {
|
||||
return
|
||||
}
|
||||
opts := m[1]
|
||||
if mi := regexp.MustCompile(`-Xms(\S+)`).FindStringSubmatch(opts); len(mi) == 2 {
|
||||
initSize = mi[1]
|
||||
}
|
||||
if mx := regexp.MustCompile(`-Xmx(\S+)`).FindStringSubmatch(opts); len(mx) == 2 {
|
||||
maxSize = mx[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// setHeapEnv 设置 heap.env 中的堆内存配置
|
||||
func (s *App) setHeapEnv(content string, req ConfigTune) string {
|
||||
// 读取已有值作为默认
|
||||
oldNsInit, oldNsMax := s.parseHeapLine(content, "ROCKETMQ_NAMESRV_HEAP")
|
||||
oldBrInit, oldBrMax := s.parseHeapLine(content, "ROCKETMQ_BROKER_HEAP")
|
||||
|
||||
nsInit := req.NamesrvHeapInitSize
|
||||
if nsInit == "" {
|
||||
nsInit = oldNsInit
|
||||
}
|
||||
if nsInit == "" {
|
||||
nsInit = "512m"
|
||||
}
|
||||
nsMax := req.NamesrvHeapMaxSize
|
||||
if nsMax == "" {
|
||||
nsMax = oldNsMax
|
||||
}
|
||||
if nsMax == "" {
|
||||
nsMax = "512m"
|
||||
}
|
||||
|
||||
brInit := req.BrokerHeapInitSize
|
||||
if brInit == "" {
|
||||
brInit = oldBrInit
|
||||
}
|
||||
if brInit == "" {
|
||||
brInit = "1g"
|
||||
}
|
||||
brMax := req.BrokerHeapMaxSize
|
||||
if brMax == "" {
|
||||
brMax = oldBrMax
|
||||
}
|
||||
if brMax == "" {
|
||||
brMax = "1g"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("ROCKETMQ_NAMESRV_HEAP=-Xms%s -Xmx%s\nROCKETMQ_BROKER_HEAP=-Xms%s -Xmx%s\n", nsInit, nsMax, brInit, brMax)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package rocketmq
|
||||
|
||||
// UpdateConfig RocketMQ 配置更新
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune RocketMQ 配置调整
|
||||
type ConfigTune struct {
|
||||
// Broker 基础
|
||||
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" 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)
|
||||
// JVM - Broker
|
||||
BrokerHeapInitSize string `form:"broker_heap_init_size" json:"broker_heap_init_size"` // -Xms (broker)
|
||||
BrokerHeapMaxSize string `form:"broker_heap_max_size" json:"broker_heap_max_size"` // -Xmx (broker)
|
||||
}
|
||||
+33
-177
@@ -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/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"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 {
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
+21
-20
@@ -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/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"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()
|
||||
@@ -94,17 +99,19 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to create passwd file: %v", err))
|
||||
return
|
||||
}
|
||||
if _, err = shell.Execf(`echo 's3fs#%s %s fuse _netdev,allow_other,nonempty,url=%s,passwd_file=/etc/passwd-s3fs-%s 0 0' >> /etc/fstab`, req.Bucket, req.Path, req.URL, cast.ToString(id)); err != nil {
|
||||
if _, err = shell.Execf(`echo 's3fs#%s %s fuse3 _netdev,allow_other,url=%s,passwd_file=/etc/passwd-s3fs-%s 0 0' >> /etc/fstab`, req.Bucket, req.Path, req.URL, cast.ToString(id)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if _, err = shell.Execf("mount -a"); err != nil {
|
||||
_, _ = shell.Execf(`sed -i 's@^s3fs#%s\s%s.*$@@g' /etc/fstab`, req.Bucket, req.Path)
|
||||
_ = os.Remove("/etc/passwd-s3fs-" + cast.ToString(id))
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if _, err = shell.Execf(`df -h | grep '%s'`, req.Path); err != nil {
|
||||
_, _ = shell.Execf(`sed -i 's@^s3fs#%s\s%s.*$@@g' /etc/fstab`, req.Bucket, req.Path)
|
||||
_ = os.Remove("/etc/passwd-s3fs-" + cast.ToString(id))
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("mount failed: %v", err))
|
||||
return
|
||||
}
|
||||
@@ -126,19 +133,13 @@ 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
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`fusermount -uz '%s'`, mount.Path)
|
||||
_, _ = shell.Execf(`fusermount3 -uz '%s'`, mount.Path)
|
||||
_, err2 := shell.Execf(`umount -lf '%s'`, mount.Path)
|
||||
// 卸载之后再检查下是否还有挂载
|
||||
if _, err = shell.Execf(`df -h | grep '%s'`, mount.Path); err == nil {
|
||||
@@ -176,7 +177,7 @@ func (s *App) mounts() ([]Mount, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, id := range strings.Split(ids, "\n") {
|
||||
for id := range strings.SplitSeq(ids, "\n") {
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+133
-164
@@ -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/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/os"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"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.Split(out, "\n") {
|
||||
if len(line) == 0 {
|
||||
for line := range strings.SplitSeq(out, "\n") {
|
||||
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`)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user