Compare commits
1208 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f0ab6b678 | |||
| 6902d4dd2c | |||
| 1b7406ebfa | |||
| 8d729aaf48 | |||
| c2ae9dc4a1 | |||
| 5d5633b31a | |||
| 3677d0317c | |||
| cd8a10d799 | |||
| 3926bb83c1 | |||
| e666396b03 | |||
| ad12a83c0f | |||
| bc66c49fe2 | |||
| 01a4ec8800 | |||
| 637fb83398 | |||
| ff67ace165 | |||
| c1580afadf | |||
| 9a2e68e24e | |||
| 3fbaecbe9e | |||
| a2c96993d3 | |||
| 2208ff1c29 | |||
| 146c9aadbd | |||
| 929219e487 | |||
| 7aca6587af | |||
| a8575bcda9 | |||
| bdaa3c005b | |||
| e6fe1e48f7 | |||
| 33cb41cf42 | |||
| 0df2a9107d | |||
| 7f61793b36 | |||
| 7d9de56a82 | |||
| 18148b15b0 | |||
| ad989337b3 | |||
| 81d32420c0 | |||
| 5538f7498b | |||
| 371d2b3ac0 | |||
| ff17aee372 | |||
| 9e1d525b20 | |||
| f6526c15fa | |||
| be3ae54efe | |||
| 214b804062 | |||
| e1f074f116 | |||
| a96bbf3dfb | |||
| 39d2eee676 | |||
| 8c17b295a2 | |||
| 452ef92d8f | |||
| df49e167a9 | |||
| f3aa240aa9 | |||
| d81f4df8b6 | |||
| 4c2f9f1bbc | |||
| e2080b0603 | |||
| 53eb005531 | |||
| 9ae7b5201c | |||
| 119411a3ce | |||
| 112fc30437 | |||
| dea96649d7 | |||
| 2d136e40c5 | |||
| 11e4d05181 | |||
| 50381dff0c | |||
| 8e40d9a75d | |||
| 8251a9a70e | |||
| 0297ccbf28 | |||
| a9113f3e4c | |||
| 17918c0f1f | |||
| 9509cfa4e2 | |||
| 1e32f9addc | |||
| abe2e1fd3d | |||
| cbf9bdc926 | |||
| ab4a94c32d | |||
| 0ba29c7c40 | |||
| e99c71b8e1 | |||
| b95b035991 | |||
| 5959cab965 | |||
| d4668f2a7e | |||
| e1fb498aa5 | |||
| 6413d6462c | |||
| 3f9fce7841 | |||
| 58d100e636 | |||
| 98d57b6c23 | |||
| 3a8c7784f5 | |||
| 02bfddcf03 | |||
| 55750796b3 | |||
| 692f8ca030 | |||
| fbe99ea4dd | |||
| d34438d90e | |||
| 47e114d43e | |||
| 0b9bba45d7 | |||
| 00ea2f40e2 | |||
| 5183653dbb | |||
| 9f7d8e413e | |||
| 4450d2c8d1 | |||
| b5f7458344 | |||
| da44f1a8fe | |||
| 0317aa1401 | |||
| 90fc48f478 | |||
| a8178cbcd6 | |||
| 91ecd04c27 | |||
| 6743ac9621 | |||
| 5606d980be | |||
| 34300431a4 | |||
| 6f0f4c69ef | |||
| 5fba45e3a4 | |||
| bb140ade7b | |||
| 1316c50a57 | |||
| f14b4f5c54 | |||
| b08eb6bf1f | |||
| 7f5869f805 | |||
| ecadaa9bbc | |||
| 60b9350625 | |||
| d64f099f8e | |||
| 294a78cd95 | |||
| 8294a1a74e | |||
| c403efd762 | |||
| cf8dfca4c0 | |||
| e552caa38f | |||
| ddff13990a | |||
| 27f79ea7a3 | |||
| 04d570197a | |||
| 8e74428023 | |||
| f4c09324b7 | |||
| ee6a12fac2 | |||
| ba429068ad | |||
| 711ccb4504 | |||
| 7d6e078e20 | |||
| d3a7c9e29c | |||
| 10d22bfbc0 | |||
| 724d05d980 | |||
| 690e7f0391 | |||
| a28fc592d1 | |||
| ff7e76ac2b | |||
| f2eb3a180c | |||
| 020a4ad870 | |||
| b5e7cfc069 | |||
| 52f8a6f045 | |||
| 3831d47a23 | |||
| 3a579f2346 | |||
| e85e8dffb3 | |||
| e18d40b0ab | |||
| c308b8e42c | |||
| cafded526f | |||
| 99aa6c99d8 | |||
| e0daee4766 | |||
| bf8c832b32 | |||
| 2d35cbc176 | |||
| 6b51f58856 | |||
| f58c680ced | |||
| 9c6d0b5837 | |||
| d069cbec75 | |||
| 9b252aa8f6 | |||
| a52d06e3cb | |||
| c25c78bfef | |||
| 567f3ab693 | |||
| aa162e12db | |||
| 7169b90c1d | |||
| 5e1001f10f | |||
| 8e26589c80 | |||
| 27ec65650d | |||
| e2345a8285 | |||
| 4ebe20d7f8 | |||
| 8f5872b284 | |||
| 75c6522453 | |||
| ff34cc72cc | |||
| e2fb1080da | |||
| b96c6ad899 | |||
| c1a4fb0904 | |||
| 5c50da27c5 | |||
| 336a9bc675 | |||
| ea3bf37687 | |||
| 79e09f9cb7 | |||
| d73c988073 | |||
| 9641ae7f34 | |||
| fd2c89a480 | |||
| 526f192200 | |||
| 6423953aeb | |||
| 1091ec8a23 | |||
| 2cbc641b2d | |||
| f4d23211e3 | |||
| 1b6086bb0b | |||
| f399e9fcec | |||
| 17cf1ada0e | |||
| 85bd71aefe | |||
| f01afacf34 | |||
| 52e8830605 | |||
| e90c6f4f7b | |||
| eb94ef89b3 | |||
| adc6dccaae | |||
| 855b2e87f9 | |||
| 3c601b808f | |||
| 8652501e95 | |||
| 43b67f67be | |||
| 4fff0b84d8 | |||
| fd73982f86 | |||
| 10191ae5d6 | |||
| b162f71d47 | |||
| a9f34a5783 | |||
| 1e4285b516 | |||
| 44c370a119 | |||
| a1c0b4a399 | |||
| 805f6c0002 | |||
| 5066446053 | |||
| 0c31641ebd | |||
| 343d451752 | |||
| 12ecf0707e | |||
| 899ce1fa18 | |||
| 9ef56f6ede | |||
| 4b3ebc082c | |||
| fb3c8ccc85 | |||
| f620a8737e | |||
| d0d5b237ef | |||
| 11f5adc5a4 | |||
| 6927a0256c | |||
| dbc78343dc | |||
| d8347310ea | |||
| 8d6b3fe95e | |||
| 036e01795e | |||
| 2d7faaa653 | |||
| 5921a88c75 | |||
| d6f4b8736b | |||
| 227f10751c | |||
| 54cc556d17 | |||
| 0d8a26ccb6 | |||
| eb18b1080a | |||
| ffe7b95b9c | |||
| ca097cac1e | |||
| f3a578380a | |||
| beb271b3b9 | |||
| 310316c8e4 | |||
| 7521b0f788 | |||
| 2783e2b28e | |||
| cf26063287 | |||
| 2a61f8939f | |||
| 513a298541 | |||
| 1cbc53b311 | |||
| aed35990bc | |||
| 9179543b7f | |||
| f40ad8bdf9 | |||
| 56ae7ebfa0 | |||
| cf5f7bd866 | |||
| bd6496c1cb | |||
| 3c75c1c49d | |||
| 2b0f8a57bb | |||
| 658b6a79eb | |||
| 42dd00c5ea | |||
| 8fa5cec568 | |||
| 2af4b9569d | |||
| 408b9ce19b | |||
| b2845fba15 | |||
| ba819a5a9c | |||
| bf1359e3d6 | |||
| 2970aaaa47 | |||
| a65bcf582a | |||
| 145387b3df | |||
| f7c586e05b | |||
| 96c78b6be9 | |||
| 46ae37a4a1 | |||
| d56b41bfef | |||
| 68beffff22 | |||
| a774253ec3 | |||
| 9dfc1d3058 | |||
| 049f708cae | |||
| b749a8599f | |||
| 95cf05879d | |||
| 9ac5ae4968 | |||
| 2491f34b3f | |||
| 302d0f1bec | |||
| 5ac43df821 | |||
| 8a1e7ef88b | |||
| b0a4df7ca6 | |||
| 70f41b87ea | |||
| ea96634318 | |||
| cfff459673 | |||
| bba50692b3 | |||
| 595cb03c67 | |||
| fa5817febd | |||
| 9a9fdbdca6 | |||
| 2225e59ca6 | |||
| 5dce30523b | |||
| 965c39ee08 | |||
| 3507517d5b | |||
| 17cce7ec2a | |||
| a2b30b07bb | |||
| ffb7bfda51 | |||
| d36dd8fded | |||
| a5f69db353 | |||
| 4606c63c81 | |||
| 247000718d | |||
| aedde0d433 | |||
| 0b61f2f67c | |||
| b166022cea | |||
| fb88060824 | |||
| f760dbaf80 | |||
| 6050bf611b | |||
| 0b425f09ee | |||
| f18c39ff60 | |||
| 9db30ac11b | |||
| 3c8f606d45 | |||
| 59c08f0850 | |||
| 29a7aa4ca5 | |||
| 8eb75a921e | |||
| 86fb1c392e | |||
| df4e47cc7b | |||
| 6101bedee3 | |||
| 817e86466c | |||
| 710601600a | |||
| 63a934510e | |||
| f3aac307d0 | |||
| e32c887bd7 | |||
| bc37511b15 | |||
| 72652eb814 | |||
| 6e3db107ec | |||
| 81843470b7 | |||
| a539532ae2 | |||
| fc1584e7da | |||
| 3c72a66c1f | |||
| 346ec7ab1f | |||
| 7850b74966 | |||
| f2d2bad52f | |||
| 0786451c79 | |||
| 0291ae7637 | |||
| de76376e97 | |||
| de21179d61 | |||
| b81f3de01d | |||
| be3bae7d57 | |||
| ffb4e21f48 | |||
| 39624d8e1c | |||
| 1378413e54 | |||
| 730aea037c | |||
| 5ae6135429 | |||
| 84703391a3 | |||
| 354705ec5b | |||
| f182eab2dc | |||
| 2828795fb0 | |||
| e6aad21631 | |||
| ff239c467a | |||
| 892f91be3e | |||
| d51d11f460 | |||
| f9233bd36b | |||
| a2ebc070ae | |||
| 480a6a829d | |||
| 47b92a8b2a | |||
| 1a7f679fca | |||
| 8b6b94ea1f | |||
| 0d9c8b3a2d | |||
| 2be97a4543 | |||
| 7b6b86f0f6 | |||
| 6f82242d92 | |||
| 7f301cec58 | |||
| c75cfe9eb6 | |||
| b94664ed87 | |||
| 93fc5e08f6 | |||
| 9bcfd4cd73 | |||
| 2d10110eef | |||
| 78e31fdc40 | |||
| d2b1394950 | |||
| 389b0a005b | |||
| 0b1554c2c6 | |||
| 3b580e2827 | |||
| 595f5ba2d9 | |||
| 78543dac67 | |||
| e9cbc8e945 | |||
| 905e206f9c | |||
| b5b1992e3b | |||
| a7fe0c87a5 | |||
| 5dcbac1c93 | |||
| 67a6d5cea8 | |||
| f6fec06e82 | |||
| 2d74e8966e | |||
| 065877547e | |||
| 12b6a1af35 | |||
| b36f7f3085 | |||
| 2112e86329 | |||
| f2c3569447 | |||
| 1a9fe40b80 | |||
| df021819b4 | |||
| 57749f03b9 | |||
| d97697efde | |||
| bc28719b10 | |||
| db79d17919 | |||
| 4e6654af33 | |||
| 4015c7e1bc | |||
| d30874a5d4 | |||
| f4a97c1740 | |||
| 7cb497929b | |||
| 8cb74159f2 | |||
| 3b75f7d2ac | |||
| 87c65176aa | |||
| 3b3cb1ff24 | |||
| a701ebcad6 | |||
| 3d88983feb | |||
| 39a9bad2e1 | |||
| 7b0e98bf02 | |||
| 324c61625d | |||
| b50a3f4b4e | |||
| 3de3b40d88 | |||
| 722c4f3812 | |||
| 9cf3a0e2b6 | |||
| c80c22c133 | |||
| 5acf1e6eb7 | |||
| cdeaf9f48f | |||
| afebeb7c00 | |||
| 0d73f2919d | |||
| 070ad97e95 | |||
| b7eba92b2b | |||
| 2c339b9923 | |||
| 8d0ea50d4f | |||
| d6a1c13328 | |||
| 789e6e2043 | |||
| d692b9ac13 | |||
| eb362e42e3 | |||
| 3ca106ec87 | |||
| 377baa0783 | |||
| 117ccabbb5 | |||
| b718c11f3a | |||
| 41d3d3fb97 | |||
| 31ad944da8 | |||
| a25de6e4db | |||
| 3af6d4d47d | |||
| 59a2fef9e8 | |||
| 3d22a1bc03 | |||
| e64c27a821 | |||
| f2bdba10bb | |||
| e18d07e694 | |||
| 50ba0ce38d | |||
| 8b2c7ea778 | |||
| e640a3cedd | |||
| 31ab4f0fb6 | |||
| 8dee17b539 | |||
| af3172739e | |||
| 182d9b1c8c | |||
| d03facb556 | |||
| 577fb370a7 | |||
| 7ef8fc6787 | |||
| e5ba1375b1 | |||
| e8a01e2d04 | |||
| bdee27541c | |||
| f807da175d | |||
| ae29541d02 | |||
| 0f844ac3ea | |||
| 744d0b9617 | |||
| 1bea44146f | |||
| a7a68f6b34 | |||
| 80bfe50335 | |||
| f235492f8b | |||
| e1bcabec5d | |||
| 8a03006772 | |||
| e497f5f7d7 | |||
| 5668bc1afd | |||
| bf9c0bc12e | |||
| 7677c3b50e | |||
| 82a74a538b | |||
| d84e3f75e3 | |||
| ce155af56b | |||
| ebc5fb9796 | |||
| 5e237a6bce | |||
| 567663a6f5 | |||
| 38935607d6 | |||
| 0af6b22862 | |||
| 787bb80355 | |||
| 1785b8ee1a | |||
| a223280172 | |||
| e1cceca340 | |||
| b9d5aa5fb1 | |||
| 3fae54bf58 | |||
| 1b2f9ea1d2 | |||
| fb94c015b2 | |||
| 1ae8011660 | |||
| 2978df4981 | |||
| 99cd473680 | |||
| d18bf93571 | |||
| 86e4cff989 | |||
| 6072332e1a | |||
| 8721cff4df | |||
| cab338c4c4 | |||
| de7dd8834f | |||
| 1d9e2761f3 | |||
| 6102742377 | |||
| 641747230b | |||
| 3c1c4e92b9 | |||
| 6ec5ebb18d | |||
| 198bd39664 | |||
| b561e241a5 | |||
| ff3b755faa | |||
| 7e7b180370 | |||
| 497e357d65 | |||
| 09698f7cd3 | |||
| 119183971d | |||
| 96d7e14631 | |||
| 0c4d97a512 | |||
| 3c39027120 | |||
| 8f6b2647a3 | |||
| ecbf8dd51a | |||
| 8a8e6302da | |||
| 2e45b6c5ab | |||
| 962098b1f7 | |||
| 7817756569 | |||
| 6738f3e83d | |||
| 07bb0961da | |||
| ac6c57b394 | |||
| 5082a4f9f9 | |||
| cfc1d46381 | |||
| 3157011cc0 | |||
| 48b06b25da | |||
| f04ae48347 | |||
| 87d26fbb23 | |||
| 6cfb7f4e0b | |||
| bee61facad | |||
| 342bf62ef6 | |||
| 9cd80f8d1b | |||
| f9b53175a1 | |||
| 6704612482 | |||
| d31f40f2dc | |||
| e02de8ba05 | |||
| 95f0e0d536 | |||
| 7e18d75203 | |||
| 3de66d884c | |||
| a911002953 | |||
| c5d0b31251 | |||
| f77e4d17fc | |||
| 6892cff96f | |||
| 4337bc2718 | |||
| 82145021b7 | |||
| d0d98f273c | |||
| 7ff18ae0a9 | |||
| 7a5d2352fa | |||
| dc0314fc28 | |||
| 9ca2445d4a | |||
| ba42e33b72 | |||
| 06c0c08ead | |||
| dc96f9d4ac | |||
| e7a54cbe85 | |||
| 39f03106df | |||
| 24ec5d0bb3 | |||
| 22cd9f8ce1 | |||
| c5efc2fb33 | |||
| 24b633d24c | |||
| 6f994e2282 | |||
| 946a8198a3 | |||
| 39ff8c9040 | |||
| 65ebecafdb | |||
| af5e79ce37 | |||
| a37393b688 | |||
| c18a96e439 | |||
| 1208144605 | |||
| 07c1c110d9 | |||
| dd66274997 | |||
| dc0174c64e | |||
| 67055a5aac | |||
| 54470f87b0 | |||
| 41a61a2c15 | |||
| 17390a026f | |||
| 3fce818d13 | |||
| 169c0ff18e | |||
| 6a30d56d7d | |||
| 4ad7ec58be | |||
| d4feba17d0 | |||
| e5f36df4bf | |||
| e1b53e88ec | |||
| d12aabd4d3 | |||
| 54c893d6e6 | |||
| 4e01168f3c | |||
| 5bb43dbf0f | |||
| dcb0d667ea | |||
| af8ef1abc8 | |||
| 2fc19dd97e | |||
| b0e577b42a | |||
| 051b6b9227 | |||
| 92ac563d75 | |||
| b7b775971e | |||
| 32266ca7c8 | |||
| 73387d114e | |||
| 3271557561 | |||
| 9a295241c3 | |||
| a00b44c485 | |||
| 93b72ad45f | |||
| a86b35f587 | |||
| 4054c788f3 | |||
| 881eb7bfb1 | |||
| fcc35c19ee | |||
| 7c3b5c02ba | |||
| bb70b25565 | |||
| bc2e07bcd8 | |||
| aa74f30893 | |||
| 27687d7dbf | |||
| f6f21d7b70 | |||
| 0815507aa8 | |||
| fb40b140c9 | |||
| eedf07ec2a | |||
| 5c214625c3 | |||
| 0402805f24 | |||
| 40715dd8a3 | |||
| 505e6d4794 | |||
| 04f3925e6d | |||
| c77fa3f0ea | |||
| 51ede84e0a | |||
| 053ef46f95 | |||
| 11d58d838f | |||
| 13b15ccb32 | |||
| 255acb392c | |||
| 6c16901dbc | |||
| 284a4c67d4 | |||
| 484ad397e7 | |||
| 407203dcfa | |||
| dded5e26aa | |||
| d029e6c58e | |||
| c1c0cbbac5 | |||
| 0fffe3a644 | |||
| 2e26af435a | |||
| ef2ecfcc80 | |||
| 6fe4d82619 | |||
| 29562974ce | |||
| b7f169634b | |||
| 87e3011f18 | |||
| cc714a5007 | |||
| 615744e0cf | |||
| 3193a15131 | |||
| 194287554e | |||
| 97b1d1a4bb | |||
| b150f61862 | |||
| b82bc21d1b | |||
| 6e9a4963af | |||
| b4a3708fa0 | |||
| 9bca8173f2 | |||
| 6520aba081 | |||
| 2903d00550 | |||
| a53ba36db8 | |||
| e9f2854d59 | |||
| 43cd3bf38d | |||
| 325a218e20 | |||
| 847e55bc8b | |||
| 1ebddd289c | |||
| e3f8fe8fe9 | |||
| b9996ba41e | |||
| bb1cd025f1 | |||
| a3059eb1cb | |||
| 192a1044ed | |||
| bbc6e3c7cc | |||
| 0aaea53af6 | |||
| 2ad9f48a5b | |||
| 0e715a7576 | |||
| 724610a45d | |||
| f65cf9e38e | |||
| 4a8ebfbdfa | |||
| fee0fcb998 | |||
| 1e081fcdf2 | |||
| 6c6fb09270 | |||
| 51b47818c0 | |||
| 5b393819ba | |||
| f807d0cd1a | |||
| 0d6f0f317d | |||
| 6bad7f4b5b | |||
| 7a7134b26a | |||
| 28cf7098ff | |||
| b56979147e | |||
| 2ec0d9e844 | |||
| 66641a626e | |||
| 95bfd6843b | |||
| 7e28a34b7f | |||
| b2152edeeb | |||
| 00815979d2 | |||
| c4e01b37b7 | |||
| 7907345521 | |||
| 8f32d6cdb0 | |||
| c3582903bf | |||
| 7d25d2f5aa | |||
| 46fc7b1c6a | |||
| 34ac62d1ae | |||
| 30e78ccf46 | |||
| 22f4cc7fcb | |||
| 1771ae682d | |||
| d1beb60ffd | |||
| cba5f86667 | |||
| e1d9832ce0 | |||
| 6278535902 | |||
| 32fb87243b | |||
| 5f85e7fbe0 | |||
| c36104a075 | |||
| 583f5d13c7 | |||
| b453738daf | |||
| 98431fb4b2 | |||
| 4d76e9286d | |||
| c70758339f | |||
| ff524cdb0b | |||
| 73c95d880a | |||
| 8a1d8c0b05 | |||
| 2251175906 | |||
| 69edde98c8 | |||
| 29a3db640d | |||
| ee87bf65b4 | |||
| aa080dab15 | |||
| b4e5498b48 | |||
| 63838bffa1 | |||
| fa41b18ced | |||
| 31bd15217b | |||
| e6b6f06706 | |||
| 1b836297bf | |||
| 96bea2cf90 | |||
| 8aff513284 | |||
| 368e41a512 | |||
| 924fdee7c5 | |||
| 438ba1db1e | |||
| d4e52a2d87 | |||
| 4432a12c7a | |||
| a75e36e447 | |||
| 05674ee04e | |||
| 09703761eb | |||
| dfa14c3fd4 | |||
| 7b80f034ea | |||
| 71923093fd | |||
| aae3cf0518 | |||
| ed22e79552 | |||
| 15dd5bb718 | |||
| 0e7af2914e | |||
| 7ec378829b | |||
| f2e8ff86c6 | |||
| eed03d644f | |||
| d9be877710 | |||
| 10439d7d97 | |||
| 55eaa26970 | |||
| 0d769d3400 | |||
| 7ff084e5a8 | |||
| 89e133c626 | |||
| c876f4c484 | |||
| 85c38e5bc5 | |||
| 6bd07251fc | |||
| 11d6b40dce | |||
| aa9b55d1eb | |||
| 2e97812776 | |||
| e0a7ada6d5 | |||
| d48963ab50 | |||
| 59bfed66dc | |||
| 5c0bc8a1b7 | |||
| ae9e39d0fe | |||
| 49b72aa656 | |||
| e0f642cbff | |||
| eb2c6cab81 | |||
| 0a3d3828c8 | |||
| 46b431de30 | |||
| 8a5b46ad7e | |||
| 5bd14946de | |||
| f6c091c725 | |||
| f4dc94ad9a | |||
| 8bcc0c0c05 | |||
| 0be04e1859 | |||
| bec9336f2f | |||
| 0be4ce9891 | |||
| 634a9a3219 | |||
| 1f2a32f883 | |||
| 32a56aa2dc | |||
| 8185a69354 | |||
| d41862f63b | |||
| 20e5122425 | |||
| 17bb8ad62b | |||
| 43624de2e4 | |||
| cc6a45b127 | |||
| 7a4a71f0a0 | |||
| 27cbb92a06 | |||
| 5ddbdfc5df | |||
| b3b1b0971f | |||
| 9caad365e9 | |||
| 5b54232e16 | |||
| a510bdb05d | |||
| 50a8b16832 | |||
| 2f56241da1 | |||
| 8557259b53 | |||
| 1d7ea0280a | |||
| 10145da2f9 | |||
| 6ba7d01875 | |||
| 763fa7379d | |||
| a36eaf7e9a | |||
| ec10cd30a5 | |||
| 847fae5caa | |||
| b3b8c22634 | |||
| 5f504d49ef | |||
| 44045da4eb | |||
| baf90c0602 | |||
| f7e67c3797 | |||
| d7a79fa53e | |||
| b9b3b9fedf | |||
| e27940a71b | |||
| aa22933b36 | |||
| a1dceaaa82 | |||
| ebd5d37e78 | |||
| 107d2ec658 | |||
| 3a70d80bb5 | |||
| 7c58d8dcce | |||
| bde93b488c | |||
| 77aa4bea34 | |||
| 18de16047d | |||
| f7ba602a50 | |||
| 3101486a44 | |||
| b51a8e1d03 | |||
| 83f58e0a2e | |||
| e941ab6661 | |||
| 688801eebb | |||
| 8ad42f8c0e | |||
| 02c8809785 | |||
| fd0e43fb26 | |||
| 96ec795096 | |||
| 5ea191b550 | |||
| c37cc3bd2c | |||
| 110f305f16 | |||
| 179b6ff83e | |||
| 0da1b0b565 | |||
| 0da3f7b8e0 | |||
| c6f9a585b0 | |||
| 686bdd9ae2 | |||
| e3a22f924e | |||
| 0c3c47361b | |||
| c3f9f56f11 | |||
| 76e992cc9e | |||
| 73cc5a35ad | |||
| 873520e9f4 | |||
| af3254c11a | |||
| 5575d34ab4 | |||
| a995c56f1b | |||
| 1fbea148e3 | |||
| ca504e1acc | |||
| 18fe84117d | |||
| 9eb9a2c7d6 | |||
| de7d50ce51 | |||
| 6e0d93257f | |||
| 96806d4e94 | |||
| 3de78c95bf | |||
| 367348e688 | |||
| 07dfcd778c | |||
| 789d5e7ddd | |||
| 1102aba9a3 | |||
| fddb076136 | |||
| b1ff8fff3d | |||
| 5cd68fcdc3 | |||
| 3a5ff9179a | |||
| fa7c54ada5 | |||
| 94047174d1 | |||
| 305777f65a | |||
| d56d3ec1dd | |||
| 8ac8da7914 | |||
| 5d56663b69 | |||
| 7ff574cb92 | |||
| 00b4153a28 | |||
| 9412b31279 | |||
| bbf04c91cf | |||
| 252456a1cd | |||
| 8de68b317d | |||
| 8bf273e1fd | |||
| 32111ecd7a | |||
| 82b8a32133 | |||
| b82c644d81 | |||
| 06bf1b0604 | |||
| 7b29e4546a | |||
| ae6b354a78 | |||
| d96c616c61 | |||
| 213698dd2a | |||
| 39b3c74684 | |||
| b3a1af1001 | |||
| 91b244276a | |||
| b1eefd062d | |||
| 2edf6748e7 | |||
| eac69b8dab | |||
| bb7b641193 | |||
| 3b0aee1967 | |||
| 2ae7f3e458 | |||
| ee0442d159 | |||
| 363b7a55be | |||
| 3437bb2b1f | |||
| 7e000cddc8 | |||
| a18652f134 | |||
| dcd8bb3dd9 | |||
| d62a280fe9 | |||
| c3a7710e6f | |||
| 7f1491f739 | |||
| c96b88548e | |||
| f62b112e1b | |||
| e55e3402e9 | |||
| 48b539b31a | |||
| d35245aa11 | |||
| 7f46dea052 | |||
| 48ed174dc6 | |||
| a5743f94b2 | |||
| 227c5344e3 | |||
| 4c8a88ea03 | |||
| 3503aac2f6 | |||
| 6beda2ea12 | |||
| 4579bf8fa9 | |||
| 002db34b4d | |||
| e276c4f747 | |||
| f5e6219ca4 | |||
| fd92b1d360 | |||
| 5b8289c0b0 | |||
| d8fe050607 | |||
| c0310d916f | |||
| 77ed2d77e1 | |||
| c331143e9e | |||
| 71e75312ff | |||
| 0e5bee4852 | |||
| 036ad9222a | |||
| 39cd5545c5 | |||
| 4568e60133 | |||
| d2f224eaa6 | |||
| c77713b3d9 | |||
| f1e2ba5c35 | |||
| 243a36f41c | |||
| 74f772b705 | |||
| e8f1ed5dec | |||
| 634d8c643b | |||
| 1829b46466 | |||
| 478efa5016 | |||
| 841e0e351f | |||
| 0e9761f975 | |||
| 5c067e28b7 | |||
| 0a0e0bcac1 | |||
| c3732b904d | |||
| 161370ac5a | |||
| 0cfe585ad5 | |||
| 283d9dbe26 | |||
| 45ed3f2ff9 | |||
| 5703e3354f | |||
| da2e66b7c6 | |||
| 8dca4c1cbf | |||
| a295eb9a65 | |||
| 868ee73d51 | |||
| ef7d38f2af | |||
| 801ff28854 | |||
| e0a401f688 | |||
| f50f2f7f2b | |||
| 3df1388326 | |||
| 5390ef3140 | |||
| 7cb5a3db4b | |||
| 082b0d2755 | |||
| 10ba87b63a | |||
| ec4c187a11 | |||
| 2e250dbb39 | |||
| 69938cf9bc | |||
| 3bc3efe59c | |||
| 9b6c9484e4 | |||
| e127572300 | |||
| c757dd9555 | |||
| 97c3f4c7c7 | |||
| 1e8c4945d5 | |||
| 90ef29acc4 | |||
| 89a6c3733b | |||
| 26d8aacabd | |||
| e7e5d4b283 | |||
| db0e722ea3 | |||
| ccd0aa3505 | |||
| 43f19c802f | |||
| a86d8a7490 | |||
| 08d906e27c | |||
| 70d65b2b2c | |||
| 3de92c9603 | |||
| deb9d9fbd2 | |||
| 71903c1100 | |||
| 9853c1792b | |||
| 18f0257ff2 | |||
| 0754fcaf6a | |||
| ea7bb34be7 | |||
| 383b1652cf | |||
| ca36c7c62a | |||
| c42fc0c70d | |||
| a89f5e3ca3 | |||
| 64537e1c75 | |||
| 19a57f9951 | |||
| 95a90db230 | |||
| 5dcfe4126c | |||
| d3e758f61a | |||
| d921939186 | |||
| b0a5221aed | |||
| dd4db4899d | |||
| 7f0b7b349a | |||
| 2a483bc9cd | |||
| 731a1f13a9 | |||
| 95025267aa | |||
| 4dd1bd3d46 | |||
| 633fa61738 | |||
| 35c998f402 | |||
| 1a82279304 | |||
| ac4380ef14 | |||
| 6b8228a048 | |||
| 67927eef06 | |||
| 429a027c79 | |||
| 284f6fb953 | |||
| 8194028a76 | |||
| c173f57c6d | |||
| 639159f150 | |||
| 89bd5d4b36 | |||
| 165424b4b2 | |||
| d519ede9e9 | |||
| ebe2700cfd | |||
| bf56151db5 | |||
| 53e13d1257 | |||
| 2732a476fd | |||
| e8b7128f7a | |||
| ec9bd0ebb3 | |||
| e1da619460 | |||
| a90b800ede | |||
| d64403f49a | |||
| 527ef39a92 | |||
| 82d2fab895 | |||
| bdf33163f0 | |||
| 705620ab13 | |||
| 6e2d3eed06 | |||
| 053de1b848 | |||
| 721d579512 | |||
| 8b8fbaa3ec | |||
| 4a8a4dbcde | |||
| cd241f88af | |||
| 064bf7e1ed | |||
| c1aa6e2e17 | |||
| 246e53c519 | |||
| 0810eee37a | |||
| c003b11ff3 | |||
| d39743cdb9 | |||
| 94751d474f | |||
| 63033dfa15 | |||
| 8dc41d4222 | |||
| 396fb1207e | |||
| 0ce8b15aca | |||
| 55fc516049 | |||
| a9c19115d4 | |||
| 9375860203 | |||
| 52aaadb2f9 | |||
| c7ad1a1435 | |||
| c0db1cf993 | |||
| bad23197c1 | |||
| f6dc38e61b | |||
| 40ce8a2aad | |||
| 999fda846d | |||
| 958369b5ce | |||
| dc86e2efa3 | |||
| bb23be565a | |||
| 2a40cb11f7 | |||
| 4da6969636 | |||
| bb5dd8a835 | |||
| 7af5ab3c30 | |||
| 6a328c3a42 | |||
| 51ec5e137b | |||
| 8684727267 | |||
| c884dd6fd7 | |||
| 35d2838036 | |||
| 0acf996981 | |||
| 2be12f20f2 | |||
| 07b0c7a010 | |||
| cf4bc94e6d | |||
| d89f9553e9 | |||
| e4536360bd | |||
| 8bfef8030c | |||
| 69e972fa5e | |||
| a33bd8309c | |||
| dacdfc6d45 | |||
| 1c4e34e8fe | |||
| c9f650ff66 | |||
| 255f94d588 | |||
| a3aaaa37ef | |||
| f31e3a7d8e | |||
| 1e462bf0e6 | |||
| 1ed79a86b2 | |||
| 0e4d593d6c | |||
| 849e6602dd | |||
| 4dc16b1e03 | |||
| 599f3b5cb4 | |||
| ccc50ed6ee | |||
| 8e84d2553f | |||
| d731efb082 | |||
| 11ad8e524e | |||
| 87e975df28 | |||
| 00d0cadb03 | |||
| 10db77ffd3 | |||
| 3c82631d7b | |||
| cd27c05d1c | |||
| 3a8f850a50 | |||
| 06e66eaa0a | |||
| 2fcd3db4a4 | |||
| 2ec74e774b | |||
| b26f75f3a3 | |||
| 7e685dd43c | |||
| 3d3dcbef8d | |||
| bc2166ee66 | |||
| 7cf577ff5b | |||
| 87cdda782e | |||
| 24106288cb | |||
| 3f258a5f81 | |||
| ce8addfecd | |||
| d969360b54 | |||
| 1f43a12f81 | |||
| 930b86a671 | |||
| 790e56003e | |||
| c38736abb6 | |||
| befad1d3ac | |||
| 53d8c2ff6d | |||
| d07314b5be | |||
| f04cafbcee | |||
| 86199b9eec | |||
| e4e3ad5821 | |||
| a80ddfaebc | |||
| 026e1319fe | |||
| b532064cf0 | |||
| f900ada2c6 | |||
| 468b20348a | |||
| 862686e4e2 | |||
| 5a3a1e694b | |||
| a368117e00 | |||
| 596cd68728 | |||
| d55bb5ffe1 | |||
| 94ff9e2dbc | |||
| f4cb1a850a | |||
| 7d1acae966 | |||
| 18b524bddc | |||
| 70596a7906 | |||
| fbf0b7acce | |||
| 12ad6e8879 | |||
| b4ea470099 | |||
| bb9f629b8e | |||
| fb099b1124 | |||
| 8584f0809e | |||
| 8ae3751f0b | |||
| 0e0fbb9a93 | |||
| 7349e027e2 | |||
| b5ba1018fa | |||
| b1479b9051 | |||
| 3a9b2ab10a | |||
| f61f1fa213 | |||
| 73c0f41e3d | |||
| 361199ddb5 | |||
| a86badc64d | |||
| 154e0afbc3 | |||
| 58a13f0636 | |||
| 4ab4f273e7 | |||
| 5e7859f063 | |||
| 50662ecfa1 | |||
| 007bbb0ce4 | |||
| 6b36e1659c | |||
| 4b4c2c4a0b | |||
| fc9ca25a46 | |||
| ff60fb44b7 | |||
| 37f677bca3 | |||
| a9000f851c | |||
| 437b88eaf2 | |||
| 12563e0cd2 | |||
| f7d65fc22e | |||
| 83cbae034c | |||
| 2b1b58ea2b | |||
| 1c5b32a7a9 | |||
| edd526d3b8 | |||
| 4c41ec036c | |||
| ac420debbb | |||
| 46e027b18c | |||
| fe98a5be1b | |||
| 63c4e27806 | |||
| b769032990 | |||
| e56fcac3b8 | |||
| b0a6405b8e | |||
| 43f12873c0 | |||
| 89004f4d04 | |||
| b0c072f78b | |||
| 8559fb9d0c | |||
| 1bbec2b637 | |||
| 1426fb48ff | |||
| 0509397332 | |||
| 57de4cb988 | |||
| d05d3eb2aa | |||
| 876fdc419f | |||
| 3f64d81250 | |||
| c4d304f2c9 | |||
| 33d5d48b59 | |||
| d90423db4a | |||
| e032faeae0 | |||
| 70601dfb00 | |||
| c6263ebe73 | |||
| 485b7b9e8d | |||
| 4c4a605684 | |||
| a347b5470d | |||
| 0a3573bfb7 | |||
| 01620ebd1d | |||
| 57ad59093d | |||
| 4339ad7c50 | |||
| 5916ad98bd | |||
| b8ccfbb4f5 | |||
| 786985341b | |||
| 1cb5a61ff2 | |||
| f6f47785ed | |||
| 3d43a2a08f | |||
| 00ec8d693c | |||
| 1fa96e828e | |||
| c46656ed2d | |||
| 47a9e6728f | |||
| b343071ec2 | |||
| 5383623203 | |||
| 31fff122c3 | |||
| ebc0c31611 | |||
| 52e5bc6277 | |||
| 6892fcde43 | |||
| 7ef2aef377 | |||
| 4b61cad486 | |||
| ad9076162b | |||
| cf82d3c1c1 | |||
| b8f5eccf9c | |||
| 914e4e1b7b | |||
| 2fd37af6ea | |||
| 30b7a8f317 | |||
| f757862ebe | |||
| 462b2f96d3 | |||
| ef79f043a3 | |||
| 7b0221a7a6 | |||
| fe04657c71 | |||
| 77dbf006fe | |||
| 2202e1a8a2 | |||
| 83ebe763d2 | |||
| c82bd280aa | |||
| fe4a6e2e78 | |||
| 08a443333f |
@@ -1,32 +1,69 @@
|
||||
# Config file for [Air](https://github.com/air-verse/air) in TOML format
|
||||
|
||||
# Working directory
|
||||
# . or absolute path, please note that the directories following must be under root.
|
||||
root = "."
|
||||
tmp_dir = "storage/temp"
|
||||
|
||||
[build]
|
||||
bin = "./storage/temp/main.exe --env=panel.conf"
|
||||
cmd = "go build -o ./storage/temp/main.exe ."
|
||||
delay = 1000
|
||||
exclude_dir = ["storage", "database"]
|
||||
exclude_file = []
|
||||
exclude_regex = []
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
# Array of commands to run before each build
|
||||
pre_cmd = []
|
||||
# Just plain old shell command. You could use `make` as well.
|
||||
cmd = "go build -o storage/temp/main.exe ./cmd/web"
|
||||
# Array of commands to run after ^C
|
||||
post_cmd = []
|
||||
# Binary file yields from `cmd`.
|
||||
bin = "storage/temp/main.exe"
|
||||
# Customize binary, can setup environment variables when run your app.
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
# Watch these filename extensions.
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
kill_delay = "0s"
|
||||
# Ignore these filename extensions or directories.
|
||||
exclude_dir = ["storage", "web"]
|
||||
# Watch these directories if you specified.
|
||||
include_dir = []
|
||||
# Watch these files.
|
||||
include_file = []
|
||||
# Exclude files.
|
||||
exclude_file = []
|
||||
# Exclude specific regular expressions.
|
||||
exclude_regex = ["_test\\.go"]
|
||||
# Exclude unchanged files.
|
||||
exclude_unchanged = true
|
||||
# Follow symlink for directories
|
||||
follow_symlink = true
|
||||
# This log file places in your tmp_dir.
|
||||
log = "build-errors.log"
|
||||
send_interrupt = false
|
||||
# It's not necessary to trigger build each time file changes if it's too frequent.
|
||||
delay = 2000
|
||||
# Stop running old binary when build errors occur.
|
||||
stop_on_error = true
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
# Send Interrupt signal before killing process (windows does not support this feature)
|
||||
send_interrupt = false
|
||||
# Delay after sending Interrupt signal
|
||||
kill_delay = 500 # nanosecond
|
||||
# Rerun binary or not
|
||||
rerun = false
|
||||
# Delay after each execution
|
||||
rerun_delay = 500
|
||||
|
||||
[log]
|
||||
# Show log time
|
||||
time = false
|
||||
# Only show main log (silences watcher, build, runner)
|
||||
main_only = false
|
||||
|
||||
[color]
|
||||
# Customize each part's color. If no color found, use the raw app log.
|
||||
main = "magenta"
|
||||
watcher = "cyan"
|
||||
build = "yellow"
|
||||
runner = "green"
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
# Delete tmp directory on exit
|
||||
clean_on_exit = true
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = true
|
||||
keep_scroll = true
|
||||
@@ -0,0 +1,6 @@
|
||||
*.sh linguist-language=Go
|
||||
*.ts linguist-language=Go
|
||||
*.js linguist-language=Go
|
||||
*.css linguist-language=Go
|
||||
*.scss linguist-language=Go
|
||||
*.html linguist-language=Go
|
||||
@@ -1,4 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
open_collective: haozi-panel
|
||||
custom: ['https://afdian.net/a/haozi-team']
|
||||
open_collective: tnb
|
||||
custom: [ 'https://afdian.com/a/TheTNB' ]
|
||||
|
||||
@@ -19,15 +19,13 @@ body:
|
||||
options:
|
||||
- label: 我已经搜索了现有的 Issues, Discussions 和 Google (I've searched the existing Issues, Discussions and Google)
|
||||
required: true
|
||||
- label: 我已经阅读了 README 中除版本记录以外的所有内容 (I've read everything in the README except the version notes)
|
||||
required: true
|
||||
- label: 我在提问题之前至少花费了 5 分钟来思考和准备 (I spend at least 5 minutes thinking and preparing before asking a question)
|
||||
- label: 我已经阅读了文档中除版本记录以外的所有内容 (I've read everything in the documentation except the version notes)
|
||||
required: true
|
||||
- label: 我没有改动过面板及系统的任何文件 (I have not changed any files of the panel and system)
|
||||
required: false
|
||||
- label: 这个问题可以被稳定复现 (The problem can be stably reproduced)
|
||||
required: false
|
||||
- label: 问题是在升级之后产生的 (The problem is generated after upgrading)
|
||||
- label: 问题是在更新之后产生的 (The problem is generated after upgrading)
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: system
|
||||
@@ -37,9 +35,10 @@ body:
|
||||
请提供安装面板的操作系统。
|
||||
Please provide the operating system on which the panel is installed.
|
||||
options:
|
||||
- RockyLinux
|
||||
- AlmaLinux
|
||||
- Debian
|
||||
- Ubuntu
|
||||
- AlmaLinux
|
||||
- RockyLinux
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
@@ -50,17 +49,17 @@ body:
|
||||
description: |
|
||||
请提供操作系统的版本号。
|
||||
Please provide the version number of the operating system.
|
||||
placeholder: "9.0"
|
||||
placeholder: "12"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 耗子面板版本 (HaoZi Panel Version)
|
||||
label: 耗子面板版本 (Rat Panel Version)
|
||||
description: |
|
||||
请提供面板的版本号。
|
||||
Please provide the version number of the panel.
|
||||
placeholder: "2.0.0"
|
||||
placeholder: "2.3.0"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -68,7 +67,7 @@ body:
|
||||
attributes:
|
||||
label: 描述问题 (Describe The Problem)
|
||||
description: |
|
||||
简明概要地描述你遇到的问题。
|
||||
简明概要地描述您遇到的问题。
|
||||
Briefly describe the problem you are having.
|
||||
validations:
|
||||
required: true
|
||||
@@ -91,7 +90,7 @@ body:
|
||||
attributes:
|
||||
label: 预期行为 (Expected Behavior)
|
||||
description: |
|
||||
简明概要地描述你期望发生的事情。
|
||||
简明概要地描述您期望发生的事情。
|
||||
Briefly describe what you expect to happen.
|
||||
validations:
|
||||
required: true
|
||||
@@ -104,8 +103,8 @@ body:
|
||||
Please copy and paste any relevant log output.
|
||||
可以把文件拖入这个区域以添加日志文件。
|
||||
Files can be dragged into this area to add log files.
|
||||
面板日志文件可在 `/www/panel/storage/logs` 中找到。
|
||||
Panel log files can be found in `/www/panel/storage/logs`.
|
||||
面板日志文件可在安装目录 `panel/storage/logs` 中找到。
|
||||
Panel log files can be found in the installation directory `panel/storage/logs`.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
@@ -113,7 +112,7 @@ body:
|
||||
attributes:
|
||||
label: 截图 (Screenshots)
|
||||
description: |
|
||||
如果有,添加屏幕截图可帮助更快定位你的问题。
|
||||
如果有,添加屏幕截图可帮助更快定位您的问题。
|
||||
If so, adding screenshots can help locate your issue faster.
|
||||
可以复制图片后在此区域内粘贴以添加图片。
|
||||
Pictures can be copied and pasted in this area to add pictures.
|
||||
@@ -128,16 +127,7 @@ body:
|
||||
description: |
|
||||
运行环境?浏览器?软件版本?相关的配置?链接?参考资料?
|
||||
Environment? Browser? Software version? Related configuration? Link? References?
|
||||
任何能让我们对你所遇到的问题有更多了解的东西。
|
||||
任何能让我们对您所遇到的问题有更多了解的东西。
|
||||
Anything that can give us more insight into the problem you're having.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: yellows
|
||||
attributes:
|
||||
label: 来点色图 (Porn Pictures)
|
||||
description: |
|
||||
如果你不提供色图而你的 Issue 中又有其他不符合标准的地方,那么你的 Issue 可能会被直接关闭。
|
||||
If you don't provide some porn pictures and there are other non-conformities in your issue, then your issue may be closed directly.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -25,7 +25,7 @@ body:
|
||||
attributes:
|
||||
label: 描述功能 (Describe Feature)
|
||||
description: |
|
||||
简明概要地描述你的新功能,以及它将解决什么问题。
|
||||
简明概要地描述您的新功能,以及它将解决什么问题。
|
||||
Briefly describe your new feature and what problem it will solve.
|
||||
validations:
|
||||
required: true
|
||||
@@ -53,12 +53,3 @@ body:
|
||||
Anything that can help us more with the implementation of this feature.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: yellows
|
||||
attributes:
|
||||
label: 来点色图 (Porn Pictures)
|
||||
description: |
|
||||
如果你不提供色图而你的 Issue 中又有其他不符合标准的地方,那么你的 Issue 可能会被直接关闭。
|
||||
If you don't provide some porn pictures and there are other non-conformities in your issue, then your issue may be closed directly.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 819 KiB |
|
After Width: | Height: | Size: 64 KiB |
@@ -0,0 +1,55 @@
|
||||
name: Backend
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
goarch: [ amd64, arm64 ]
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Wait for frontend build
|
||||
uses: lewagon/wait-on-check-action@v1.3.4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
check-name: 'build (frontend)'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Download frontend
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
workflow: frontend.yml
|
||||
name: frontend
|
||||
path: internal/embed/frontend
|
||||
check_artifacts: true
|
||||
- name: Build ${{ matrix.goarch }}
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: |
|
||||
go build -ldflags '-s -w --extldflags "-static"' -o web-${{ matrix.goarch }} ./cmd/web
|
||||
go build -ldflags '-s -w --extldflags "-static"' -o cli-${{ matrix.goarch }} ./cmd/cli
|
||||
- name: Compress ${{ matrix.goarch }}
|
||||
run: |
|
||||
upx --best --lzma web-${{ matrix.goarch }}
|
||||
upx --best --lzma cli-${{ matrix.goarch }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.goarch }}
|
||||
path: |
|
||||
web-${{ matrix.goarch }}
|
||||
cli-${{ matrix.goarch }}
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
goarch: [ amd64, arm64 ]
|
||||
fail-fast: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Build ${{ matrix.goarch }}
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: linux
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: go build -ldflags '-s -w --extldflags "-static -fpic"' -o panel-${{ matrix.goarch }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: panel-${{ matrix.goarch }}
|
||||
path: panel-${{ matrix.goarch }}
|
||||
@@ -1,23 +0,0 @@
|
||||
name: Codecov
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
codecov:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Run tests with coverage
|
||||
run: go test -v -coverprofile="coverage.out" ./...
|
||||
- name: Upload coverage report to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.out
|
||||
token: ${{ secrets.CODECOV }}
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Frontend
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
name: build (frontend)
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
run_install: true
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: web/pnpm-lock.yaml
|
||||
- name: Build frontend
|
||||
# We need to run the dev server first to generate the auto-imports files
|
||||
run: |
|
||||
cp .env.production .env
|
||||
cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
pnpm dev &
|
||||
sleep 5
|
||||
kill %1
|
||||
pnpm build
|
||||
- name: Upload frontend
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend
|
||||
path: web/dist/ # https://github.com/actions/upload-artifact/issues/541
|
||||
@@ -7,27 +7,34 @@ permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Fetch Frontend
|
||||
run: |
|
||||
sudo apt-get install -y curl jq unzip zip
|
||||
curl -sSL https://api.github.com/repos/haozi-team/panel-frontend/releases/latest | jq -r ".assets[] | select(.name | contains(\"dist\")) | .browser_download_url" | xargs curl -L -o frontend.zip
|
||||
rm -rf public
|
||||
unzip frontend.zip
|
||||
mv dist public
|
||||
- name: Wait for frontend build
|
||||
uses: lewagon/wait-on-check-action@v1.3.4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
check-name: 'build (frontend)'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Download frontend
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
workflow: frontend.yml
|
||||
name: frontend
|
||||
path: internal/embed/frontend
|
||||
check_artifacts: true
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Issue Auto Lock
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 */6 * * *"
|
||||
workflow_dispatch:
|
||||
issues:
|
||||
types: [ opened ]
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
jobs:
|
||||
issue-lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set GH_REPO
|
||||
run: echo "GH_REPO=${{ github.repository }}" >> $GITHUB_ENV
|
||||
- name: Auto Lock Issue
|
||||
uses: devhaozi/issue-auto-lock@v1
|
||||
with:
|
||||
gh_repo: ${{ github.repository }}
|
||||
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue_labels: "⭐ No Star"
|
||||
@@ -1,18 +1,15 @@
|
||||
name: Issue Auto Reply
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [ labeled ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
issue-reply:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: ✏️ Feature
|
||||
if: github.event.label.name == '✏️ Feature'
|
||||
@@ -27,7 +24,7 @@ jobs:
|
||||
我们认为您的建议非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献!
|
||||
We think your suggestion is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution!
|
||||
|
||||

|
||||

|
||||
- name: ☢️ Bug
|
||||
if: github.event.label.name == '☢️ Bug'
|
||||
uses: actions-cool/issues-helper@v3
|
||||
@@ -41,4 +38,4 @@ jobs:
|
||||
我们认为您的反馈非常有价值!欢迎提交 PR,请包含相应的测试用例、文档等,并确保 CI 通过,感谢和期待您的贡献!
|
||||
We think your feedback is very valuable! Welcome to submit a PR, please include test cases, documentation, etc., and ensure that the CI is passed, thank you and look forward to your contribution!
|
||||
|
||||

|
||||

|
||||
@@ -1,25 +0,0 @@
|
||||
name: Issue Close Question
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
issue-close-require:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: needs more info
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: 'close-issues'
|
||||
labels: 'question'
|
||||
inactive-day: 3
|
||||
body: |
|
||||
由于该 Issue 3天未收到回应,现已被自动关闭,若有任何问题,可评论回复。
|
||||
This issue has been closed automatically because it has not had recent activity for 3 days. If you have any questions, please comment here.
|
||||
@@ -7,20 +7,73 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
golangci:
|
||||
name: golanci-lint
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
cache: false
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
skip-cache: true
|
||||
skip-pkg-cache: true
|
||||
skip-build-cache: true
|
||||
version: latest
|
||||
args: --timeout=30m ./...
|
||||
nilaway:
|
||||
runs-on: ubuntu-24.04
|
||||
if: false
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install dependencies
|
||||
run: go mod tidy
|
||||
- name: Install NilAway
|
||||
run: go install go.uber.org/nilaway/cmd/nilaway@latest
|
||||
- name: Run NilAway
|
||||
run: nilaway -include-pkgs="github.com/TheTNB/panel" ./...
|
||||
govulncheck:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install Govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
- name: Run Govulncheck
|
||||
run: govulncheck ./...
|
||||
frontend:
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: latest
|
||||
run_install: true
|
||||
package_json_file: web/package.json
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: web/pnpm-lock.yaml
|
||||
- name: Run pnpm lint
|
||||
run: pnpm lint
|
||||
@@ -5,18 +5,25 @@ on:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
unit:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v4
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
cache: true
|
||||
go-version: 'stable'
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y curl jq
|
||||
- name: Set up environment
|
||||
run: |
|
||||
cp panel-example.conf .env
|
||||
go run . artisan key:generate
|
||||
cp config.example.yml config.yml
|
||||
- name: Run tests
|
||||
run: go test ./...
|
||||
run: sudo go test -v -coverprofile="coverage.out" ./...
|
||||
- name: Upload coverage report to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
file: ./coverage.out
|
||||
token: ${{ secrets.CODECOV }}
|
||||
@@ -1,10 +1,6 @@
|
||||
tmp
|
||||
/.air.toml
|
||||
/panel.conf
|
||||
|
||||
# Golang #
|
||||
# `go test -c` 生成的二进制文件
|
||||
*.test
|
||||
|
||||
# go coverage 工具
|
||||
*.out
|
||||
*.prof
|
||||
@@ -14,16 +10,15 @@ _cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
# 编译文件 #
|
||||
# 编译文件
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
*.exe
|
||||
*.o
|
||||
*.so
|
||||
/panel
|
||||
|
||||
# 压缩包 #
|
||||
# 压缩包
|
||||
# Git 自带压缩,如果这些压缩包里有代码,建议解压后 commit
|
||||
*.7z
|
||||
*.dmg
|
||||
@@ -34,16 +29,17 @@ _cgo_export.*
|
||||
*.tar
|
||||
*.zip
|
||||
|
||||
# 日志文件和数据库 #
|
||||
# 日志文件和数据库及配置
|
||||
*.log
|
||||
*.sqlite
|
||||
*.db
|
||||
config.yml
|
||||
|
||||
# 临时文件 #
|
||||
# 临时文件
|
||||
tmp/
|
||||
.tmp/
|
||||
|
||||
# 系统生成文件 #
|
||||
# 系统生成文件
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
.AppleDouble
|
||||
@@ -58,7 +54,7 @@ Thumbs.db
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# IDE 和编辑器 #
|
||||
# IDE 和编辑器
|
||||
.idea/
|
||||
/go_build_*
|
||||
out/
|
||||
@@ -67,8 +63,3 @@ out/
|
||||
*.sublime*
|
||||
__debug_bin
|
||||
.project
|
||||
|
||||
# 前端工具链 #
|
||||
.sass-cache/*
|
||||
node_modules/
|
||||
/public
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
image: golang:bookworm
|
||||
|
||||
# 在每个任务执行前运行
|
||||
before_script:
|
||||
- mkdir -p .go
|
||||
- go version
|
||||
- go env -w GO111MODULE=on
|
||||
- go env -w GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
.go_cache:
|
||||
variables:
|
||||
GOPATH: $CI_PROJECT_DIR/.go
|
||||
cache:
|
||||
paths:
|
||||
- .go/pkg/mod/
|
||||
|
||||
# 全局变量
|
||||
variables:
|
||||
OUTPUT_NAME: "panel"
|
||||
GO111MODULE: "on"
|
||||
GOPROXY: "https://goproxy.cn,direct"
|
||||
|
||||
stages:
|
||||
- prepare
|
||||
- build
|
||||
- release
|
||||
|
||||
golangci_lint:
|
||||
stage: prepare
|
||||
image: golangci/golangci-lint:latest-alpine
|
||||
extends: .go_cache
|
||||
allow_failure: true
|
||||
script:
|
||||
- golangci-lint run --timeout 30m
|
||||
|
||||
unit_test:
|
||||
stage: prepare
|
||||
extends: .go_cache
|
||||
allow_failure: true
|
||||
script:
|
||||
- rm -rf /etc/apt/sources.list
|
||||
- rm -rf /etc/apt/sources.list.d/*
|
||||
- wget -O /etc/apt/sources.list https://mirrors.ustc.edu.cn/repogen/conf/debian-http-4-bookworm
|
||||
- apt-get update
|
||||
- apt-get install -y curl jq
|
||||
- cp panel-example.conf .env
|
||||
- go run . artisan key:generate
|
||||
- go test -v -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
build:
|
||||
stage: build
|
||||
extends: .go_cache
|
||||
script:
|
||||
- go mod download
|
||||
- CGO_ENABLED=0 go build -ldflags '-s -w --extldflags "-static -fpic"' -o $OUTPUT_NAME
|
||||
artifacts:
|
||||
name: "$OUTPUT_NAME"
|
||||
paths:
|
||||
- $OUTPUT_NAME
|
||||
expire_in: 3 days
|
||||
|
||||
fetch:
|
||||
stage: build
|
||||
image: alpine:latest
|
||||
before_script:
|
||||
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories
|
||||
- apk add --no-cache curl jq unzip zip
|
||||
script:
|
||||
- curl -sSL "https://jihulab.com/api/v4/projects/haozi-team%2Fpanel-frontend/releases" | jq -r '.[0].assets.links[] | select(.name | contains("dist")) | .direct_asset_url' | xargs curl -L -o frontend.zip
|
||||
- rm -rf public
|
||||
- unzip frontend.zip
|
||||
- mv dist public
|
||||
artifacts:
|
||||
name: "frontend"
|
||||
paths:
|
||||
- public
|
||||
expire_in: 3 days
|
||||
|
||||
release:
|
||||
stage: release
|
||||
dependencies:
|
||||
- build
|
||||
- fetch
|
||||
image:
|
||||
name: goreleaser/goreleaser
|
||||
entrypoint: [ '' ]
|
||||
only:
|
||||
- tags
|
||||
variables:
|
||||
# Disable shallow cloning so that goreleaser can diff between tags to
|
||||
# generate a changelog.
|
||||
GIT_DEPTH: 0
|
||||
script:
|
||||
- goreleaser release --clean
|
||||
@@ -1,50 +1,55 @@
|
||||
version: 2
|
||||
project_name: panel
|
||||
|
||||
builds:
|
||||
-
|
||||
id: panel
|
||||
binary: panel
|
||||
- id: web
|
||||
main: ./cmd/web
|
||||
binary: web
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
- GOPROXY=https://goproxy.cn,direct
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
goamd64:
|
||||
- v2
|
||||
ldflags:
|
||||
- -s -w --extldflags "-static -fpic"
|
||||
- -s -w --extldflags "-static"
|
||||
- id: cli
|
||||
main: ./cmd/cli
|
||||
binary: cli
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w --extldflags "-static"
|
||||
|
||||
upx:
|
||||
- enabled: true
|
||||
# Filter by build ID.
|
||||
ids:
|
||||
- web
|
||||
- cli
|
||||
# Compress argument.
|
||||
# Valid options are from '1' (faster) to '9' (better), and 'best'.
|
||||
compress: best
|
||||
# Whether to try LZMA (slower).
|
||||
lzma: true
|
||||
# Whether to try all methods and filters (slow).
|
||||
brute: false
|
||||
|
||||
archives:
|
||||
-
|
||||
id: panel-archive
|
||||
- id: panel
|
||||
builds:
|
||||
- panel
|
||||
- web
|
||||
- cli
|
||||
format: zip
|
||||
wrap_in_directory: false
|
||||
strip_parent_binary_folder: true
|
||||
strip_binary_directory: true
|
||||
files:
|
||||
- LICENSE
|
||||
- public/*
|
||||
- config.example.yml
|
||||
- storage/*
|
||||
- database/*
|
||||
- scripts/*
|
||||
- panel-example.conf
|
||||
|
||||
gitlab_urls:
|
||||
api: https://jihulab.com/api/v4/
|
||||
download: https://jihulab.com
|
||||
|
||||
# set to true if you use a self-signed certificate
|
||||
skip_tls_verify: false
|
||||
|
||||
# set to true if you want to upload to the Package Registry rather than attachments
|
||||
# Only works with GitLab 13.5+
|
||||
#
|
||||
# Since: v1.3
|
||||
use_package_registry: true
|
||||
|
||||
# Set this if you set GITLAB_TOKEN to the value of CI_JOB_TOKEN.
|
||||
#
|
||||
# Since: v1.11
|
||||
use_job_token: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## 行为准则
|
||||
|
||||
耗子Linux面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
|
||||
耗子面板遵守业界通用的行为准则。任何违反行为准则的行为都可以报告给我们:
|
||||
|
||||
- 参与者将容忍反对意见。
|
||||
- 参与者必须确保他们的语言和行为没有人身攻击和贬低个人言论。
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
The HaoZi Linux Panel complies with the industry's common code of conduct. Any breach of the Code of Conduct can be reported to us:
|
||||
The Rat Panel complies with the industry's common code of conduct. Any breach of the Code of Conduct can be reported to us:
|
||||
|
||||
- Participants will be tolerant of opposing views.
|
||||
- Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
||||
|
||||
@@ -1,131 +1,178 @@
|
||||
<h1 align="center">耗子 Linux 面板</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/haozi-team/panel/releases"><img src="https://img.shields.io/github/release/haozi-team/panel.svg"></a>
|
||||
<a href="https://github.com/haozi-team/panel/actions"><img src="https://github.com/haozi-team/panel/actions/workflows/test.yml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/haozi-team/panel"><img src="https://goreportcard.com/badge/github.com/haozi-team/panel"></a>
|
||||
<a href="https://codecov.io/gh/haozi-team/panel"><img src="https://codecov.io/gh/haozi-team/panel/branch/main/graph/badge.svg?token=XFT5NGNSRG"></a>
|
||||
<a href="https://img.shields.io/github/license/haozi-team/panel"><img src="https://img.shields.io/github/license/haozi-team/panel"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<p align="right">
|
||||
[简体中文] | [<a href="README_EN.md">English</a>]
|
||||
</p>
|
||||
|
||||
耗子 Linux 面板是针对我们自身业务需要使用 Golang 开发的轻量 Linux 服务器运维管理面板,以 GNU Affero General Public License v3.0 开源。
|
||||
<h1 align="center" style="font-size: 40px">耗子面板</h1>
|
||||
|
||||
免责声明:严禁使用耗子 Linux 面板从事任何非法活动,非法站点请勿向我们请求任何形式的技术支持,如果在技术支持过程中发现非法内容,我们将立即停止技术支持并留存相关证据。
|
||||
<p align="center">
|
||||
<a href="https://github.com/TheTNB/panel/releases"><img src="https://img.shields.io/github/release/TheTNB/panel.svg"></a>
|
||||
<a href="https://github.com/TheTNB/panel/actions"><img src="https://github.com/TheTNB/panel/actions/workflows/test.yml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/TheTNB/panel"><img src="https://goreportcard.com/badge/github.com/TheTNB/panel"></a>
|
||||
<a href="https://img.shields.io/github/license/TheTNB/panel"><img src="https://img.shields.io/github/license/TheTNB/panel"></a>
|
||||
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2FTheTNB%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2FTheTNB%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
|
||||
</p>
|
||||
|
||||
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ频道:[pd.qq.com/s/fyol46wfy](https://pd.qq.com/s/fyol46wfy)
|
||||
耗子面板是新一代企业级全能服务器运维管理面板。简单轻量,高效运维。
|
||||
|
||||
## 重要说明
|
||||
**赞助:[爱发电](https://afdian.com/a/TheTNB)** | QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 微信群:[复制此链接](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | 论坛:[tom.moe](https://tom.moe)
|
||||
|
||||
目前我们正在重构面板前端部分开发 2.1 版本,v2.0.58 将是 2.0 的最后一个版本。
|
||||
## 优势
|
||||
|
||||
新前端将使用 Vue + Naive UI 开发,预计 10 月底可以完成。
|
||||
1. **极低占用:** 在 Debian 下部署面板 + LNMP 环境,内存占用不到 500 MB,遥遥领先于使用容器化的其他面板。
|
||||
2. **低破坏性:** 面板的设计理念是尽可能减少对系统的额外修改,在同类面板中,我们对系统的修改最少。
|
||||
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障。
|
||||
4. **高效运维:** 面板界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
|
||||
5. **离线运行:** 面板运行可不依赖任何外部服务,您甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
|
||||
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 2 年无事故。
|
||||
7. **开源开放:** 面板开源,您可以自由修改、审计面板源码,安全性有保障。
|
||||
|
||||
存量用户请尽快更新至 v2.0.58,以便后续升级到 2.1 版本。
|
||||
## UI 截图
|
||||
|
||||

|
||||
|
||||
## 运行环境
|
||||
|
||||
耗子Linux面板仅支持 `amd64` | `arm64` 架构下的主流系统的最新版本,不支持 `Ubuntu`,因为其发版太过频繁,难以维护。
|
||||
耗子面板支持 `amd64` | `arm64` 架构下的主流系统,下表中的系统均已测试 LNMP 环境安装。
|
||||
|
||||
低配机器建议使用 `Debian`,资源占用较 `RHEL` 系更低。其他机器建议使用 `AlmaLinux` | `RockyLinux`,维护周期更长也更稳定。
|
||||
优先建议使用标注**推荐**的系统,无特殊情况不建议使用标注**不推荐**的系统。
|
||||
|
||||
不在下表中的其他系统(OpenCloudOS 8、Anolis 8、CentOS Stream 8/9、Debian 11等),可自行尝试安装,但不保证能够正常运行,且不提供无偿技术支持(理论上不会有大问题)。
|
||||
不在下表中的其他系统,可自行尝试安装,但不提供技术支持(接受相关 PR 提交)。
|
||||
|
||||
CentOS Stream 可使用迁移脚本迁移至支持的系统: [CentOS 8/9 迁移脚本](https://github.com/haozi-team/byecentos)
|
||||
| 系统 | 版本 | 备注 |
|
||||
|---------------------|-----|-----|
|
||||
| AlmaLinux | 9 | 推荐 |
|
||||
| AlmaLinux | 8 | 不推荐 |
|
||||
| RockyLinux | 9 | 支持 |
|
||||
| RockyLinux | 8 | 不推荐 |
|
||||
| CentOS Stream | 9 | 不推荐 |
|
||||
| CentOS Stream | 8 | 不推荐 |
|
||||
| Ubuntu | 24 | 推荐 |
|
||||
| Ubuntu | 22 | 支持 |
|
||||
| Debian | 12 | 推荐 |
|
||||
| Debian | 11 | 支持 |
|
||||
| OpenCloudOS | 9 | 支持 |
|
||||
| TencentOS Server | 4 | 支持 |
|
||||
| TencentOS Server | 3.1 | 不推荐 |
|
||||
| Alibaba Cloud Linux | 3.2 | 不推荐 |
|
||||
| Anolis | 8 | 不推荐 |
|
||||
| openEuler | 22 | 不推荐 |
|
||||
|
||||
| 系统 | 版本 |
|
||||
|------------|----|
|
||||
| RHEL | 9 |
|
||||
| AlmaLinux | 9 |
|
||||
| RockyLinux | 9 |
|
||||
| Debian | 12 |
|
||||
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的健壮性。
|
||||
|
||||
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的稳定性。
|
||||
## 挂载硬盘
|
||||
|
||||
如果您的服务器有未挂载的数据盘,可在安装前以`root`用户登录服务器运行以下命令自动挂载,面板安装后不支持跨目录迁移。
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
|
||||
```
|
||||
|
||||
## 安装面板
|
||||
|
||||
安装面板前,你需要了解LNMP环境的基本知识,以及如何处理常见的LNMP环境问题,不建议0基础的用户安装和使用耗子Linux面板([推荐: 宝塔 - 简单好用服务器运维面板](https://www.bt.cn/?invite_code=M190eXRpZWE=))。
|
||||
> **Warning**
|
||||
> 安装面板前,您需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
|
||||
|
||||
如果你决定继续,请以`root`用户登录服务器,执行以下命令安装面板:
|
||||
以`root`用户登录服务器,运行以下命令安装面板:
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
|
||||
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
|
||||
```
|
||||
|
||||
## 卸载面板
|
||||
|
||||
优先建议备份数据重装系统,这样可以保证系统纯净。
|
||||
|
||||
如果你无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
|
||||
如果您无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum 验证失败,文件可能被篡改,已终止操作"
|
||||
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
|
||||
```
|
||||
|
||||
卸载面板前请务必备份好所有数据,提前卸载面板全部插件。卸载后数据将**无法恢复**!
|
||||
卸载面板前请务必备份好所有数据,提前卸载面板全部应用。卸载后数据将**无法恢复**!
|
||||
|
||||
## 日常维护
|
||||
|
||||
使用`panel`命令进行日常维护:
|
||||
使用`panel-cli`命令进行日常维护:
|
||||
|
||||
```shell
|
||||
panel
|
||||
panel-cli
|
||||
```
|
||||
|
||||
在 [Wiki](https://github.com/haozi-team/panel/wiki) 中查看更多使用方法和技巧。
|
||||
在 [文档](https://tom.moe/docs?category=57) 中查看更多使用方法和技巧。
|
||||
|
||||
## 问题反馈
|
||||
|
||||
使用类问题,可在 [WePublish 社区论坛](https://wepublish.cn/forums) 提问或QQ群`@坤坤`寻求 AI 帮助,亦可在群里寻求付费支持。
|
||||
使用类问题,可在 [Moe Tom](https://tom.moe) 提问或寻求 AI 帮助,亦可在群里寻求付费支持。
|
||||
|
||||
面板自身问题,可在 GitHub 的`Issues`页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。
|
||||
|
||||
## 贡献代码
|
||||
|
||||
### 寻找/创建 Issue
|
||||
|
||||
您可以在 [Issue 列表](https://github.com/haozi-team/panel/issues) 中寻找或创建一个 Issue,留言表达想要处理该 Issue 的意愿,得到维护者的确认后,即可开始处理。
|
||||
|
||||
### 创建 PR
|
||||
|
||||
- 在开发过程中,如果遇到问题可以随时在 Issue 中详尽描述该问题,以进一步沟通,但在此之前请确保自己已通过 Google 等方式尽可能的尝试解决问题;
|
||||
- PR 须提交至我们的极狐 GitLab 仓库[https://jihulab.com/haozi-team/panel](https://jihulab.com/haozi-team/panel),勿在 GitHub 上提交;
|
||||
- 当 PR 开发完毕后,请为其添加 `🚀 Review Ready` 标签,维护者将及时进行评审;
|
||||
- 我们非常欢迎您的贡献,将在下次发版时将您添加到首页贡献者中;❤️
|
||||
|
||||
## 赞助商
|
||||
|
||||
如果耗子面板对您有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
|
||||
|
||||
**同时接受云资源赞助,可通过QQ群咨询联系**
|
||||
|
||||
### 资金
|
||||
|
||||
<a href="https://www.weixiaoduo.com/">
|
||||
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
|
||||
</a>
|
||||
|
||||
### 服务器
|
||||
|
||||
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
|
||||
<img height="80" src=".github/assets/dk.png" alt="林枫云">
|
||||
</a>
|
||||
|
||||
### CDN
|
||||
|
||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
|
||||
</a>
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
|
||||
</a>
|
||||
<a href="https://scdn.ddunyun.com/">
|
||||
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
|
||||
</a>
|
||||
|
||||
- [盾云CDN](http://cdn.ddunyun.com/)
|
||||
### 合作伙伴
|
||||
|
||||
### 对象存储
|
||||
<a href="https://1ms.run">
|
||||
<img height="80" src=".github/assets/1ms.svg" alt="毫秒镜像提供经过审核的 Docker 镜像加速服务">
|
||||
</a>
|
||||
|
||||
- [又拍云](https://www.upyun.com/?utm_source=lianmeng&utm_medium=referral)
|
||||
### 赞助商们
|
||||
|
||||
### DevOps
|
||||
|
||||
- [极狐 GitLab](https://www.jihulab.com/)
|
||||
|
||||
**接受云资源和资金赞助,可通过QQ群咨询联系**
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://afdian.com/a/TheTNB">
|
||||
<img alt="sponsors" src="https://github.com/TheTNB/sponsor/blob/main/sponsors.svg?raw=true"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 贡献者
|
||||
|
||||
这个项目的存在要归功于所有做出贡献的人,参与贡献请先查看贡献代码部分。
|
||||
|
||||
<a href="https://github.com/DevHaoZi" target="_blank"><img src="https://avatars.githubusercontent.com/u/115467771?v=4" width="48" height="48"></a>
|
||||
<p align="center">
|
||||
<a href="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors?repo_id=572922963&limit=30" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=dark" width="655" height="auto">
|
||||
<img alt="Active Contributors of TheTNB/panel - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=light" width="655" height="auto">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Star 历史
|
||||
|
||||
<a href="https://star-history.com/#haozi-team/panel&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
|
||||
</picture>
|
||||
<a href="https://star-history.com/#TheTNB/panel&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 免责声明
|
||||
|
||||
严禁使用耗子面板从事任何非法活动,非法站点请勿向我们请求任何形式的技术支持,如果在技术支持过程中发现非法内容,我们将立即停止技术支持并留存相关证据。
|
||||
|
||||
@@ -1,54 +1,81 @@
|
||||
<h1 align="center">HaoZi Linux Panel</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/haozi-team/panel/releases"><img src="https://img.shields.io/github/release/haozi-team/panel.svg"></a>
|
||||
<a href="https://github.com/haozi-team/panel/actions"><img src="https://github.com/haozi-team/panel/actions/workflows/test.yml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/haozi-team/panel"><img src="https://goreportcard.com/badge/github.com/haozi-team/panel"></a>
|
||||
<a href="https://codecov.io/gh/haozi-team/panel"><img src="https://codecov.io/gh/haozi-team/panel/branch/main/graph/badge.svg?token=XFT5NGNSRG"></a>
|
||||
<a href="https://img.shields.io/github/license/haozi-team/panel"><img src="https://img.shields.io/github/license/haozi-team/panel"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<p align="right">
|
||||
[<a href="README.md">简体中文</a>] | [English]
|
||||
</p>
|
||||
|
||||
The HaoZi Linux Panel is a lightweight Linux server operation and maintenance management panel developed using Golang for our own business needs. It is open source under the GNU Affero General Public License v3.0 protocol.
|
||||
<h1 align="center" style="font-size: 40px">Rat Panel</h1>
|
||||
|
||||
Disclaimer: It is strictly prohibited to use the HaoZi Linux Panel to engage in any illegal activities. Please do not request any form of technical support from us for illegal sites. If illegal content is discovered during the technical support process, we will immediately stop technical support and retain relevant evidence.
|
||||
<p align="center">
|
||||
<a href="https://github.com/TheTNB/panel/releases"><img src="https://img.shields.io/github/release/TheTNB/panel.svg"></a>
|
||||
<a href="https://github.com/TheTNB/panel/actions"><img src="https://github.com/TheTNB/panel/actions/workflows/test.yml/badge.svg"></a>
|
||||
<a href="https://goreportcard.com/report/github.com/TheTNB/panel"><img src="https://goreportcard.com/badge/github.com/TheTNB/panel"></a>
|
||||
<a href="https://img.shields.io/github/license/TheTNB/panel"><img src="https://img.shields.io/github/license/TheTNB/panel"></a>
|
||||
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2FTheTNB%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2FTheTNB%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
|
||||
</p>
|
||||
|
||||
Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | QQ channel: [pd.qq.com/s/fyol46wfy](https://pd.qq.com/s/fyol46wfy)
|
||||
Rat Panel is a new generation of enterprise-level all-in-one server operation and maintenance management panel. Simple and lightweight, efficient operation and maintenance.
|
||||
|
||||
**Sponsor: [Open Collective](https://opencollective.com/tnb)** | QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | WeChat group: [Copy this link](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | Forum: [tom.moe](https://tom.moe)
|
||||
|
||||
## Advantages
|
||||
|
||||
1. **Extremely low usage:** Deploying the panel + LNMP environment under Debian, the memory usage is less than 500 MB, far ahead of other panels using containerization.
|
||||
2. **Low destructiveness:** The design concept of the panel is to minimize the additional modifications to the system. Among similar panels, we have made the least modifications to the system.
|
||||
3. **Follow the times:** All components of the panel are at the forefront of the times, updated quickly, powerful functions, and guaranteed security.
|
||||
4. **Efficient operation and maintenance:** The panel is simple and easy to operate, and you can quickly deploy various environments and adjust application settings without complicated configuration.
|
||||
5. **Offline operation:** The panel can run without relying on any external services. You can even stop the panel process after deployment is complete, and it will not affect the deployed services.
|
||||
6. **Tested by time:** We have been using it in production environment since 2022, and it has been running stably for 2 year without any accidents.
|
||||
7. **Open source and open:** The panel is open source, you can freely modify and audit the panel source code, and the security is guaranteed.
|
||||
|
||||
## UI Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
## Operating Environment
|
||||
|
||||
HaoZi Linux Panel only supports the latest version of mainstream systems under the `amd64` | `arm64` architecture. It does not support `Ubuntu` because its releases are too frequent and difficult to maintain.
|
||||
Rat Panel supports mainstream systems under the `amd64` | `arm64` architecture, and the systems in the table below have been tested for LNMP environment installation.
|
||||
|
||||
Recommended to use `Debian` for low-configuration machines, as its resource usage is lower than that of the `RHEL` system. For other machines, recommended to use `RockyLinux` | `AlmaLinux`, which has a longer maintenance cycle and is more stable.
|
||||
It is recommended to use the systems marked **Recommended** first, and not recommended to use the systems marked **Not recommended** without special circumstances.
|
||||
|
||||
For other systems not in the table below (OpenCloudOS 8, Anolis 8, CentOS Stream 8/9, Debian 11, etc.), you can try to install it yourself, but normal operation is not guaranteed, and free technical support is not provided (theoretically there will be no major question).
|
||||
For other systems not listed in the table below, you can try to install them by yourself, but there is no technical support will be provided (accept related PR submissions).
|
||||
|
||||
CentOS Stream can be migrated to a supported system using the migration script: [CentOS 8/9 Migration Script](https://github.com/haozi-team/byecentos)
|
||||
| OS | Version | Note |
|
||||
|---------------------|---------|-----------------|
|
||||
| AlmaLinux | 9 | Recommended |
|
||||
| AlmaLinux | 8 | Not recommended |
|
||||
| RockyLinux | 9 | Support |
|
||||
| RockyLinux | 8 | Not recommended |
|
||||
| CentOS Stream | 9 | Not recommended |
|
||||
| CentOS Stream | 8 | Not recommended |
|
||||
| Ubuntu | 24 | Recommended |
|
||||
| Ubuntu | 22 | Support |
|
||||
| Debian | 12 | Recommended |
|
||||
| Debian | 11 | Support |
|
||||
| OpenCloudOS | 9 | Support |
|
||||
| TencentOS Server | 4 | Support |
|
||||
| TencentOS Server | 3.1 | Not recommended |
|
||||
| Alibaba Cloud Linux | 3.2 | Not recommended |
|
||||
| Anolis | 8 | Not recommended |
|
||||
| openEuler | 22 | Not recommended |
|
||||
|
||||
| OS | Version |
|
||||
|------------|---------|
|
||||
| RHEL | 9 |
|
||||
| RockyLinux | 9 |
|
||||
| AlmaLinux | 9 |
|
||||
| Debian | 12 |
|
||||
As system versions are constantly updated, we may also terminate support for some older systems to ensure the robustness of the panel.
|
||||
|
||||
As system versions are constantly updated, we may also terminate support for some older systems to ensure the stability of the panel.
|
||||
## Mount Disk
|
||||
|
||||
If your server has an unmounted data disk, you can run the following command as the `root` user to automatically mount it before installation. The panel does not support cross-directory migration after installation.
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
|
||||
```
|
||||
|
||||
## Install Panel
|
||||
|
||||
Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. It is not recommended for users with zero basic knowledge to install and use HaoZi Linux Panel ([Recommended: aaPanel - easy-to-use server operation and maintenance panel](https://aapanel.com)).
|
||||
> **Warning**
|
||||
> Before installing the panel, you need to understand the basic knowledge of the LNMP environment and how to deal with common LNMP environment problems. We are not recommended for users with zero basic knowledge to install and use Rat Panel.
|
||||
|
||||
If you decide to continue, please log in to the server as `root` user and execute the following command to install the panel:
|
||||
Login to the server as the `root` user and run the following command to install the panel:
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/install_panel.sh.checksum.txt && sha256sum -c install_panel.sh.checksum.txt && bash install_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
|
||||
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
|
||||
```
|
||||
|
||||
## Uninstall Panel
|
||||
@@ -58,70 +85,94 @@ Recommended to back up data and reinstall the system first, so that the system c
|
||||
If you are unable to reinstall the system, log in to the server as the `root` user and execute the following command to uninstall the panel:
|
||||
|
||||
```shell
|
||||
HAOZI_DL_URL="https://jihulab.com/haozi-team/download/-/raw/main/panel"; curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh && curl -sSL -O ${HAOZI_DL_URL}/uninstall_panel.sh.checksum.txt && sha256sum -c uninstall_panel.sh.checksum.txt && bash uninstall_panel.sh || echo "Checksum Verification Failed, File May Have Been Tampered With, Operation Terminated"
|
||||
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
|
||||
```
|
||||
|
||||
Before uninstalling the panel, please be sure to back up all data and uninstall all panel plugins in advance. The data will **not be recoverable** after uninstallation!
|
||||
|
||||
## Daily Maintenance
|
||||
|
||||
Use `panel` command for daily maintenance:
|
||||
Use `panel-cli` command for daily maintenance:
|
||||
|
||||
```shell
|
||||
panel
|
||||
panel-cli
|
||||
```
|
||||
|
||||
See more usage methods and tips in [Wiki](https://github.com/haozi-team/panel/wiki).
|
||||
See more usage methods and tips in [Document](https://tom.moe/docs?category=57).
|
||||
|
||||
## Feedback
|
||||
|
||||
For usage issues, you can ask questions in the [WePublish Community Forum](https://wepublish.cn/forums) or seek AI help in the QQ group `@坤坤`. You can also seek paid support in the group.
|
||||
For usage issues, you can ask questions in the [Moe Tom](https://tom.moe) or seek AI help. You can also seek paid support in the group.
|
||||
|
||||
For issues with the panel itself, you can submit feedback on the `Issues` page of GitHub. Please note [the wisdom of asking questions](http://www.catb.org/~esr/faqs/smart-questions.html).
|
||||
|
||||
## Contribute code
|
||||
|
||||
### Find/Create Issue
|
||||
|
||||
You can find or create an Issue in [Issue List](https://github.com/haozi-team/panel/issues), leave a message to express your willingness to deal with the Issue, and get confirmation from the maintainer. Start processing.
|
||||
|
||||
### Create PR
|
||||
|
||||
- During the development process, if you encounter a problem, you can describe the problem in detail in the Issue at any time for further communication, but before doing so, please make sure that you have tried your best to solve the problem through Google and other methods;
|
||||
- PRs must be submitted to our JiHu GitLab repository [https://jihulab.com/haozi-team/panel](https://jihulab.com/haozi-team/panel). Do not submit on GitHub;
|
||||
- When the PR is completed, please add the `🚀 Review Ready` tag to it, and the maintainer will review it in time;
|
||||
- We very much welcome your contributions and will add you to the homepage contributors in the next release; ❤️
|
||||
|
||||
## Sponsor
|
||||
|
||||
If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollective.com/tnb), thanks to the following supporters/sponsors:
|
||||
|
||||
**Accept cloud resources and financial sponsorship, you can contact us through QQ group**
|
||||
|
||||
### Financial
|
||||
|
||||
<a href="https://www.weixiaoduo.com/">
|
||||
<img height="80" src=".github/assets/wxd.png" alt="微晓朵">
|
||||
</a>
|
||||
|
||||
### Server
|
||||
|
||||
<a href="https://www.dkdun.cn/aff/MQZZNVHQ">
|
||||
<img height="80" src=".github/assets/dk.png" alt="林枫云">
|
||||
</a>
|
||||
|
||||
### CDN
|
||||
|
||||
- [无畏云加速](https://su.sctes.com/register?code=8st689ujpmm2p)
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/sctes.png" alt="无畏云加速">
|
||||
</a>
|
||||
<a href="https://su.sctes.com/register?code=8st689ujpmm2p">
|
||||
<img height="80" src=".github/assets/wafpro.png" alt="WAFPRO">
|
||||
</a>
|
||||
<a href="https://scdn.ddunyun.com/">
|
||||
<img height="80" src=".github/assets/ddunyun.png" alt="盾云SCDN">
|
||||
</a>
|
||||
|
||||
- [盾云CDN](http://cdn.ddunyun.com/)
|
||||
### Partners
|
||||
|
||||
### Object Storage
|
||||
<a href="https://1ms.run">
|
||||
<img height="80" src=".github/assets/1ms.svg" alt="毫秒镜像提供经过审核的 Docker 镜像加速服务">
|
||||
</a>
|
||||
|
||||
- [又拍云](https://www.upyun.com/?utm_source=lianmeng&utm_medium=referral)
|
||||
### Sponsors
|
||||
|
||||
### DevOps
|
||||
|
||||
- [JiHu GitLab](https://www.jihulab.com/)
|
||||
|
||||
**Accept cloud resources and financial sponsorship, you can contact us through QQ group**
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://afdian.com/a/TheTNB">
|
||||
<img alt="sponsors" src="https://github.com/TheTNB/sponsor/blob/main/sponsors.svg?raw=true"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Contributor
|
||||
|
||||
This project owes its existence to all those who have contributed. To contribute, please check the contributed code section first.
|
||||
|
||||
<a href="https://github.com/DevHaoZi" target="_blank"><img src="https://avatars.githubusercontent.com/u/115467771?v=4" width="48" height="48"></a>
|
||||
<p align="center">
|
||||
<a href="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors?repo_id=572922963&limit=30" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=dark" width="655" height="auto">
|
||||
<img alt="Active Contributors of TheTNB/panel - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=572922963&limit=30&image_size=auto&color_scheme=light" width="655" height="auto">
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#haozi-team/panel&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=haozi-team/panel&type=Date" />
|
||||
</picture>
|
||||
<a href="https://star-history.com/#TheTNB/panel&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=TheTNB/panel&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## Disclaimer
|
||||
|
||||
It is strictly prohibited to use the Rat Panel to engage in any illegal activities. Please do not request any form of technical support from us for illegal sites. If illegal content is discovered during the technical support process, we will immediately stop technical support and retain relevant evidence.
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
## 安全说明
|
||||
|
||||
安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子Linux面板,至今无一例安全事故。
|
||||
**我们不对面板的安全性做任何保证**,但安全性是我们最关心的问题之一,我们已在多个不同应用的生产环境广泛应用耗子面板,至今无任何安全事故。
|
||||
|
||||
耗子Linux面板采用业界多种方案尽可能保证面板的安全性,但是我们不能保证面板的绝对安全性,**因此我们不对面板的安全性做任何保证**。
|
||||
如果您在面板中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
|
||||
|
||||
如果您在使用面板的过程中发现任何安全问题,请勿提交 Issue,可通过以下方式直接联系我们:
|
||||
|
||||
- 邮箱:i@haozi.net
|
||||
- 邮箱:[admin@haozi.net](mailto:admin@haozi.net)
|
||||
- QQ:826896000
|
||||
|
||||
致某些 CVE 爱好者:通过面板 `access_token` 执行的任何操作(包括且不限于:获取 root 权限、读取/写入系统敏感文件、执行任意 shell 命令等)均不被认为是安全问题,请不要刷此类报告浪费彼此的时间,这类低水平的报告对你的简历也没有任何帮助。
|
||||
提前感谢您的支持与帮助!
|
||||
|
||||
致某些安全初学者:通过面板 `session` / `access_token` 执行的任何操作(包括且不限于:获取 root 权限、读取/写入系统敏感文件、执行任意 shell 命令等)均不被认为是安全问题,请不要刷此类报告浪费彼此的时间,这类低水平的报告对你的简历也没有任何帮助。
|
||||
|
||||
## Security Policy
|
||||
|
||||
Security is one of our biggest concerns. We have widely used HaoZi Linux Panel in production environments for many different applications, and there has been no safety incident so far.
|
||||
|
||||
HaoZi Linux Panel adopts various solutions in the industry to ensure the security of the panel as much as possible, but we cannot guarantee the absolute security of the panel, **therefore we do not make any guarantees on the security of the panel**.
|
||||
**We do not make any guarantees about the security of the panel**, but security is one of our top concerns. We have widely used the Rat panel in the production environment of multiple different applications, and there has been no security incidents to date.
|
||||
|
||||
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:
|
||||
|
||||
- Email: i@haozi.net
|
||||
- Email: [admin@haozi.net](mailto:admin@haozi.net)
|
||||
- Telegram: @devhaozi
|
||||
|
||||
To some CVE enthusiasts: Any operation performed through the panel `access_token` (including but not limited to: obtaining root permissions, reading/writing system sensitive files, executing arbitrary shell commands, etc.) is not considered a security issue, please Don’t waste each other’s time with these types of reports. These low-level reports won’t do anything to help your resume.
|
||||
Thank you in advance for your support and help!
|
||||
|
||||
To some security beginners: Any operation performed through the panel `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. Such low-level reports are of no help to your resume.
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/console/command"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Monitoring struct {
|
||||
}
|
||||
|
||||
// Signature The name and signature of the console command.
|
||||
func (receiver *Monitoring) Signature() string {
|
||||
return "panel:monitoring"
|
||||
}
|
||||
|
||||
// Description The console command description.
|
||||
func (receiver *Monitoring) Description() string {
|
||||
return "[面板] 系统监控"
|
||||
}
|
||||
|
||||
// Extend The console command extend.
|
||||
func (receiver *Monitoring) Extend() command.Extend {
|
||||
return command.Extend{
|
||||
Category: "panel",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Execute the console command.
|
||||
func (receiver *Monitoring) Handle(ctx console.Context) error {
|
||||
var setting models.Setting
|
||||
monitor := services.NewSettingImpl().Get(models.SettingKeyMonitor)
|
||||
if !cast.ToBool(monitor) {
|
||||
return nil
|
||||
}
|
||||
|
||||
info := tools.GetMonitoringInfo()
|
||||
|
||||
err := facades.Orm().Query().Create(&models.Monitor{
|
||||
Info: info,
|
||||
})
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板] 系统监控保存失败: %s", err.Error())
|
||||
color.Redf("[面板] 系统监控保存失败: %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// 删除过期数据
|
||||
err = facades.Orm().Query().Where("key", "monitor_days").First(&setting)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if setting.Value == "0" || len(setting.Value) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
days, err := strconv.Atoi(setting.Value)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, err = facades.Orm().Query().Where("created_at < ?", carbon.Now().SubDays(days).ToDateTimeString()).Delete(&models.Monitor{})
|
||||
if err != nil {
|
||||
facades.Log().Errorf("[面板] 系统监控删除过期数据失败: %s", err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,522 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/console/command"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Panel struct {
|
||||
}
|
||||
|
||||
// Signature The name and signature of the console command.
|
||||
func (receiver *Panel) Signature() string {
|
||||
return "panel"
|
||||
}
|
||||
|
||||
// Description The console command description.
|
||||
func (receiver *Panel) Description() string {
|
||||
return "[面板] 命令行"
|
||||
}
|
||||
|
||||
// Extend The console command extend.
|
||||
func (receiver *Panel) Extend() command.Extend {
|
||||
return command.Extend{
|
||||
Category: "panel",
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Execute the console command.
|
||||
func (receiver *Panel) Handle(ctx console.Context) error {
|
||||
action := ctx.Argument(0)
|
||||
arg1 := ctx.Argument(1)
|
||||
arg2 := ctx.Argument(2)
|
||||
arg3 := ctx.Argument(3)
|
||||
arg4 := ctx.Argument(4)
|
||||
|
||||
switch action {
|
||||
case "init":
|
||||
var check models.User
|
||||
err := facades.Orm().Query().FirstOrFail(&check)
|
||||
if err == nil {
|
||||
color.Redln("面板已初始化")
|
||||
return nil
|
||||
}
|
||||
|
||||
settings := []models.Setting{{Key: models.SettingKeyName, Value: "耗子Linux面板"}, {Key: models.SettingKeyMonitor, Value: "1"}, {Key: models.SettingKeyMonitorDays, Value: "30"}, {Key: models.SettingKeyBackupPath, Value: "/www/backup"}, {Key: models.SettingKeyWebsitePath, Value: "/www/wwwroot"}, {Key: models.SettingKeyEntrance, Value: "/"}, {Key: models.SettingKeyVersion, Value: facades.Config().GetString("panel.version")}}
|
||||
err = facades.Orm().Query().Create(&settings)
|
||||
if err != nil {
|
||||
color.Redln("初始化失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
hash, err := facades.Hash().Make(tools.RandomString(32))
|
||||
if err != nil {
|
||||
color.Redln("初始化失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
user := services.NewUserImpl()
|
||||
_, err = user.Create("admin", hash)
|
||||
if err != nil {
|
||||
color.Redln("创建管理员失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("初始化成功")
|
||||
|
||||
case "update":
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
color.Redln("当前有任务正在执行,禁止更新")
|
||||
return nil
|
||||
}
|
||||
|
||||
panel, err := tools.GetLatestPanelVersion()
|
||||
if err != nil {
|
||||
color.Redln("获取最新版本失败")
|
||||
return err
|
||||
}
|
||||
|
||||
err = tools.UpdatePanel(panel)
|
||||
if err != nil {
|
||||
color.Redln("更新失败: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("更新成功")
|
||||
tools.RestartPanel()
|
||||
|
||||
case "getInfo":
|
||||
var user models.User
|
||||
err := facades.Orm().Query().Where("id", 1).FirstOrFail(&user)
|
||||
if err != nil {
|
||||
color.Redln("获取管理员信息失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
password := tools.RandomString(16)
|
||||
hash, err := facades.Hash().Make(password)
|
||||
if err != nil {
|
||||
color.Redln("生成密码失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
user.Username = tools.RandomString(8)
|
||||
user.Password = hash
|
||||
|
||||
err = facades.Orm().Query().Save(&user)
|
||||
if err != nil {
|
||||
color.Redln("保存管理员信息失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
port := tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
|
||||
color.Greenln("用户名: " + user.Username)
|
||||
color.Greenln("密码: " + password)
|
||||
color.Greenln("面板端口: " + port)
|
||||
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
|
||||
|
||||
case "getPort":
|
||||
port := tools.Exec("cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}'")
|
||||
color.Greenln("面板端口: " + port)
|
||||
|
||||
case "getEntrance":
|
||||
color.Greenln("面板入口: " + services.NewSettingImpl().Get(models.SettingKeyEntrance, "/"))
|
||||
|
||||
case "deleteEntrance":
|
||||
err := services.NewSettingImpl().Set(models.SettingKeyEntrance, "/")
|
||||
if err != nil {
|
||||
color.Redln("删除面板入口失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("删除面板入口成功")
|
||||
|
||||
case "writePlugin":
|
||||
slug := arg1
|
||||
version := arg2
|
||||
if len(slug) == 0 || len(version) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
var plugin models.Plugin
|
||||
err := facades.Orm().Query().UpdateOrCreate(&plugin, models.Plugin{
|
||||
Slug: slug,
|
||||
}, models.Plugin{
|
||||
Version: version,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
color.Redln("写入插件安装状态失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("写入插件安装状态成功")
|
||||
|
||||
case "deletePlugin":
|
||||
slug := arg1
|
||||
if len(slug) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := facades.Orm().Query().Where("slug", slug).Delete(&models.Plugin{})
|
||||
if err != nil {
|
||||
color.Redln("移除插件安装状态失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("移除插件安装状态成功")
|
||||
|
||||
case "writeMysqlPassword":
|
||||
password := arg1
|
||||
if len(password) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
var setting models.Setting
|
||||
err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{
|
||||
Key: models.SettingKeyMysqlRootPassword,
|
||||
}, models.Setting{
|
||||
Value: password,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
color.Redln("写入MySQL root密码失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("写入MySQL root密码成功")
|
||||
|
||||
case "cleanTask":
|
||||
_, err := facades.Orm().Query().Model(&models.Task{}).Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).Update("status", models.TaskStatusFailed)
|
||||
if err != nil {
|
||||
color.Redln("清理任务失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("清理任务成功")
|
||||
|
||||
case "backup":
|
||||
backupType := arg1
|
||||
name := arg2
|
||||
path := arg3
|
||||
save := arg4
|
||||
hr := `+----------------------------------------------------`
|
||||
if len(backupType) == 0 || len(name) == 0 || len(path) == 0 || len(save) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln(hr)
|
||||
color.Greenln("★ 开始备份 [" + carbon.Now().ToDateTimeString() + "]")
|
||||
color.Greenln(hr)
|
||||
|
||||
if !tools.Exists(path) {
|
||||
tools.Mkdir(path, 0644)
|
||||
}
|
||||
|
||||
switch backupType {
|
||||
case "website":
|
||||
color.Yellowln("|-目标网站: " + name)
|
||||
var website models.Website
|
||||
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil {
|
||||
color.Redln("|-网站不存在")
|
||||
color.Greenln(hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
backupFile := path + "/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".zip"
|
||||
tools.Exec(`cd '` + website.Path + `' && zip -r '` + backupFile + `' .`)
|
||||
color.Greenln("|-备份成功")
|
||||
|
||||
case "mysql":
|
||||
rootPassword := services.NewSettingImpl().Get(models.SettingKeyMysqlRootPassword)
|
||||
backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
|
||||
|
||||
err := os.Setenv("MYSQL_PWD", rootPassword)
|
||||
if err != nil {
|
||||
color.Redln("|-备份MySQL数据库失败: " + err.Error())
|
||||
color.Greenln(hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("|-目标MySQL数据库: " + name)
|
||||
color.Greenln("|-开始导出")
|
||||
tools.Exec(`mysqldump -uroot ` + name + ` > /tmp/` + backupFile + ` 2>&1`)
|
||||
color.Greenln("|-导出成功")
|
||||
color.Greenln("|-开始压缩")
|
||||
tools.Exec("cd /tmp && zip -r " + backupFile + ".zip " + backupFile)
|
||||
tools.Remove("/tmp/" + backupFile)
|
||||
color.Greenln("|-压缩成功")
|
||||
color.Greenln("|-开始移动")
|
||||
if _, err := tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
|
||||
color.Redln("|-移动失败: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
color.Greenln("|-移动成功")
|
||||
_ = os.Unsetenv("MYSQL_PWD")
|
||||
color.Greenln("|-备份成功")
|
||||
|
||||
case "postgresql":
|
||||
backupFile := name + "_" + carbon.Now().ToShortDateTimeString() + ".sql"
|
||||
check := tools.Exec(`su - postgres -c "psql -l" 2>&1`)
|
||||
if strings.Contains(check, name) {
|
||||
color.Redln("|-数据库不存在")
|
||||
color.Greenln(hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("|-目标PostgreSQL数据库: " + name)
|
||||
color.Greenln("|-开始导出")
|
||||
tools.Exec(`su - postgres -c "pg_dump '` + name + `'" > /tmp/` + backupFile + ` 2>&1`)
|
||||
color.Greenln("|-导出成功")
|
||||
color.Greenln("|-开始压缩")
|
||||
tools.Exec("cd /tmp && zip -r " + backupFile + ".zip " + backupFile)
|
||||
tools.Remove("/tmp/" + backupFile)
|
||||
color.Greenln("|-压缩成功")
|
||||
color.Greenln("|-开始移动")
|
||||
if _, err := tools.Mv("/tmp/"+backupFile+".zip", path+"/"+backupFile+".zip"); err != nil {
|
||||
color.Redln("|-移动失败: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
color.Greenln("|-移动成功")
|
||||
color.Greenln("|-备份成功")
|
||||
}
|
||||
|
||||
color.Greenln(hr)
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
color.Redln("|-清理失败: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
var filteredFiles []os.FileInfo
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file.Name(), name) && strings.HasSuffix(file.Name(), ".zip") {
|
||||
fileInfo, err := os.Stat(filepath.Join(path, file.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
filteredFiles = append(filteredFiles, fileInfo)
|
||||
}
|
||||
}
|
||||
sort.Slice(filteredFiles, func(i, j int) bool {
|
||||
return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime())
|
||||
})
|
||||
for i := cast.ToInt(save); i < len(filteredFiles); i++ {
|
||||
fileToDelete := filepath.Join(path, filteredFiles[i].Name())
|
||||
color.Yellowln("|-清理备份: " + fileToDelete)
|
||||
tools.Remove(fileToDelete)
|
||||
}
|
||||
color.Greenln("|-清理完成")
|
||||
color.Greenln(hr)
|
||||
color.Greenln("☆ 备份完成 [" + carbon.Now().ToDateTimeString() + "]")
|
||||
color.Greenln(hr)
|
||||
|
||||
case "cutoff":
|
||||
name := arg1
|
||||
save := arg2
|
||||
hr := `+----------------------------------------------------`
|
||||
if len(name) == 0 || len(save) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln(hr)
|
||||
color.Greenln("★ 开始切割 [" + carbon.Now().ToDateTimeString() + "]")
|
||||
color.Greenln(hr)
|
||||
|
||||
color.Yellowln("|-目标网站: " + name)
|
||||
var website models.Website
|
||||
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err != nil {
|
||||
color.Redln("|-网站不存在")
|
||||
color.Greenln(hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
logPath := "/www/wwwlogs/" + website.Name + ".log"
|
||||
if !tools.Exists(logPath) {
|
||||
color.Redln("|-日志文件不存在")
|
||||
color.Greenln(hr)
|
||||
return nil
|
||||
}
|
||||
|
||||
backupPath := "/www/wwwlogs/" + website.Name + "_" + carbon.Now().ToShortDateTimeString() + ".log.zip"
|
||||
tools.Exec(`cd /www/wwwlogs && zip -r ` + backupPath + ` ` + website.Name + ".log")
|
||||
tools.Exec(`echo "" > ` + logPath)
|
||||
color.Greenln("|-切割成功")
|
||||
|
||||
color.Greenln(hr)
|
||||
files, err := os.ReadDir("/www/wwwlogs")
|
||||
if err != nil {
|
||||
color.Redln("|-清理失败: " + err.Error())
|
||||
return nil
|
||||
}
|
||||
var filteredFiles []os.FileInfo
|
||||
for _, file := range files {
|
||||
if strings.HasPrefix(file.Name(), website.Name) && strings.HasSuffix(file.Name(), ".log.zip") {
|
||||
fileInfo, err := os.Stat(filepath.Join("/www/wwwlogs", file.Name()))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
filteredFiles = append(filteredFiles, fileInfo)
|
||||
}
|
||||
}
|
||||
sort.Slice(filteredFiles, func(i, j int) bool {
|
||||
return filteredFiles[i].ModTime().After(filteredFiles[j].ModTime())
|
||||
})
|
||||
for i := cast.ToInt(save); i < len(filteredFiles); i++ {
|
||||
fileToDelete := filepath.Join("/www/wwwlogs", filteredFiles[i].Name())
|
||||
color.Yellowln("|-清理日志: " + fileToDelete)
|
||||
tools.Remove(fileToDelete)
|
||||
}
|
||||
color.Greenln("|-清理完成")
|
||||
color.Greenln(hr)
|
||||
color.Greenln("☆ 切割完成 [" + carbon.Now().ToDateTimeString() + "]")
|
||||
color.Greenln(hr)
|
||||
|
||||
case "writeSite":
|
||||
name := arg1
|
||||
status := cast.ToBool(arg2)
|
||||
path := ctx.Argument(3)
|
||||
php := cast.ToInt(ctx.Argument(4))
|
||||
ssl := cast.ToBool(ctx.Argument(5))
|
||||
if len(name) == 0 || len(path) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
var website models.Website
|
||||
if err := facades.Orm().Query().Where("name", name).FirstOrFail(&website); err == nil {
|
||||
color.Redln("网站已存在")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
color.Redln("网站目录不存在")
|
||||
return nil
|
||||
}
|
||||
|
||||
err = facades.Orm().Query().Create(&models.Website{
|
||||
Name: name,
|
||||
Status: status,
|
||||
Path: path,
|
||||
Php: php,
|
||||
Ssl: ssl,
|
||||
})
|
||||
if err != nil {
|
||||
color.Redln("写入网站失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("写入网站成功")
|
||||
|
||||
case "deleteSite":
|
||||
name := arg1
|
||||
if len(name) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := facades.Orm().Query().Where("name", name).Delete(&models.Website{})
|
||||
if err != nil {
|
||||
color.Redln("删除网站失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("删除网站成功")
|
||||
|
||||
case "writeSetting":
|
||||
key := arg1
|
||||
value := arg2
|
||||
if len(key) == 0 || len(value) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
var setting models.Setting
|
||||
err := facades.Orm().Query().UpdateOrCreate(&setting, models.Setting{
|
||||
Key: key,
|
||||
}, models.Setting{
|
||||
Value: value,
|
||||
})
|
||||
if err != nil {
|
||||
color.Redln("写入设置失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("写入设置成功")
|
||||
|
||||
case "getSetting":
|
||||
key := arg1
|
||||
if len(key) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
var setting models.Setting
|
||||
if err := facades.Orm().Query().Where("key", key).FirstOrFail(&setting); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s", setting.Value)
|
||||
|
||||
case "deleteSetting":
|
||||
key := arg1
|
||||
if len(key) == 0 {
|
||||
color.Redln("参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := facades.Orm().Query().Where("key", key).Delete(&models.Setting{})
|
||||
if err != nil {
|
||||
color.Redln("删除设置失败")
|
||||
return nil
|
||||
}
|
||||
|
||||
color.Greenln("删除设置成功")
|
||||
|
||||
default:
|
||||
color.Yellowln(facades.Config().GetString("panel.name") + "命令行工具 - " + facades.Config().GetString("panel.version"))
|
||||
color.Greenln("请使用以下命令:")
|
||||
color.Greenln("panel update 更新 / 修复面板到最新版本")
|
||||
color.Greenln("panel getInfo 重新初始化面板账号信息")
|
||||
color.Greenln("panel getPort 获取面板访问端口")
|
||||
color.Greenln("panel getEntrance 获取面板访问入口")
|
||||
color.Greenln("panel deleteEntrance 删除面板访问入口")
|
||||
color.Greenln("panel cleanTask 清理面板运行中和等待中的任务[任务卡住时使用]")
|
||||
color.Greenln("panel backup {website/mysql/postgresql} {name} {path} {save_copies} 备份网站 / MySQL数据库 / PostgreSQL数据库到指定目录并保留指定数量")
|
||||
color.Greenln("panel cutoff {website_name} {save_copies} 切割网站日志并保留指定数量")
|
||||
color.Redln("以下命令请在开发者指导下使用:")
|
||||
color.Yellowln("panel init 初始化面板")
|
||||
color.Yellowln("panel writePlugin {slug} {version} 写入插件安装状态")
|
||||
color.Yellowln("panel deletePlugin {slug} 移除插件安装状态")
|
||||
color.Yellowln("panel writeMysqlPassword {password} 写入MySQL root密码")
|
||||
color.Yellowln("panel writeSite {name} {status} {path} {php} {ssl} 写入网站数据到面板")
|
||||
color.Yellowln("panel deleteSite {name} 删除面板网站数据")
|
||||
color.Yellowln("panel getSetting {name} 获取面板设置数据")
|
||||
color.Yellowln("panel writeSetting {name} {value} 写入 / 更新面板设置数据")
|
||||
color.Yellowln("panel deleteSetting {name} 删除面板设置数据")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/console"
|
||||
"github.com/goravel/framework/contracts/schedule"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/console/commands"
|
||||
)
|
||||
|
||||
type Kernel struct {
|
||||
}
|
||||
|
||||
func (kernel *Kernel) Schedule() []schedule.Event {
|
||||
return []schedule.Event{
|
||||
facades.Schedule().Command("panel:monitoring").EveryMinute().SkipIfStillRunning(),
|
||||
}
|
||||
}
|
||||
|
||||
func (kernel *Kernel) Commands() []console.Command {
|
||||
return []console.Command{
|
||||
&commands.Panel{},
|
||||
&commands.Monitoring{},
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type CronController struct {
|
||||
cron services.Cron
|
||||
setting services.Setting
|
||||
}
|
||||
|
||||
func NewCronController() *CronController {
|
||||
return &CronController{
|
||||
cron: services.NewCronImpl(),
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取计划任务列表
|
||||
func (c *CronController) List(ctx http.Context) http.Response {
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
|
||||
var crons []models.Cron
|
||||
var total int64
|
||||
err := facades.Orm().Query().Paginate(page, limit, &crons, &total)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 查询计划任务列表失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": crons,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加计划任务
|
||||
func (c *CronController) Add(ctx http.Context) http.Response {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
"time": "required",
|
||||
"script": "required",
|
||||
"type": "required|in:shell,backup,cutoff",
|
||||
"backup_type": "required_if:type,backup|in:website,mysql,postgresql",
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
// 单独验证时间格式
|
||||
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) {
|
||||
return Error(ctx, http.StatusBadRequest, "时间格式错误")
|
||||
}
|
||||
|
||||
shell := ctx.Request().Input("script")
|
||||
cronType := ctx.Request().Input("type")
|
||||
if cronType == "backup" {
|
||||
backupType := ctx.Request().Input("backup_type")
|
||||
backupName := ctx.Request().Input("backup_database")
|
||||
if backupType == "website" {
|
||||
backupName = ctx.Request().Input("website")
|
||||
}
|
||||
backupPath := ctx.Request().Input("backup_path")
|
||||
if len(backupPath) == 0 {
|
||||
backupPath = c.setting.Get(models.SettingKeyBackupPath) + "/" + backupType
|
||||
}
|
||||
backupSave := ctx.Request().InputInt("save", 10)
|
||||
shell = `#!/bin/bash
|
||||
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 耗子Linux面板 - 数据备份脚本
|
||||
|
||||
type=` + backupType + `
|
||||
path=` + backupPath + `
|
||||
name=` + backupName + `
|
||||
save=` + cast.ToString(backupSave) + `
|
||||
|
||||
# 执行备份
|
||||
panel backup ${type} ${name} ${path} ${save} 2>&1
|
||||
`
|
||||
}
|
||||
if cronType == "cutoff" {
|
||||
website := ctx.Request().Input("website")
|
||||
save := ctx.Request().InputInt("save", 180)
|
||||
shell = `#!/bin/bash
|
||||
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:$PATH
|
||||
|
||||
# 耗子Linux面板 - 日志切割脚本
|
||||
|
||||
name=` + website + `
|
||||
save=` + cast.ToString(save) + `
|
||||
|
||||
# 执行切割
|
||||
panel cutoff ${name} ${save} 2>&1
|
||||
`
|
||||
}
|
||||
|
||||
shellDir := "/www/server/cron/"
|
||||
shellLogDir := "/www/server/cron/logs/"
|
||||
if !tools.Exists(shellDir) {
|
||||
facades.Log().Error("[面板][CronController] 计划任务目录不存在")
|
||||
return Error(ctx, http.StatusInternalServerError, "计划任务目录不存在")
|
||||
}
|
||||
if !tools.Exists(shellLogDir) {
|
||||
facades.Log().Error("[面板][CronController] 计划任务日志目录不存在")
|
||||
return Error(ctx, http.StatusInternalServerError, "计划任务日志目录不存在")
|
||||
}
|
||||
shellFile := strconv.Itoa(int(carbon.Now().Timestamp())) + tools.RandomString(16)
|
||||
if !tools.Write(shellDir+shellFile+".sh", shell, 0700) {
|
||||
facades.Log().Error("[面板][CronController] 创建计划任务脚本失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
tools.Exec("dos2unix " + shellDir + shellFile + ".sh")
|
||||
|
||||
var cron models.Cron
|
||||
cron.Name = ctx.Request().Input("name")
|
||||
cron.Type = ctx.Request().Input("type")
|
||||
cron.Status = true
|
||||
cron.Time = ctx.Request().Input("time")
|
||||
cron.Shell = shellDir + shellFile + ".sh"
|
||||
cron.Log = shellLogDir + shellFile + ".log"
|
||||
|
||||
err = facades.Orm().Query().Create(&cron)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 创建计划任务失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.cron.AddToSystem(cron)
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"id": cron.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// Script 获取脚本内容
|
||||
func (c *CronController) Script(ctx http.Context) http.Response {
|
||||
var cron models.Cron
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
}
|
||||
|
||||
return Success(ctx, tools.Read(cron.Shell))
|
||||
}
|
||||
|
||||
// Update 更新计划任务
|
||||
func (c *CronController) Update(ctx http.Context) http.Response {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
"time": "required",
|
||||
"script": "required",
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
// 单独验证时间格式
|
||||
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(ctx.Request().Input("time")) {
|
||||
return Error(ctx, http.StatusBadRequest, "时间格式错误")
|
||||
}
|
||||
|
||||
var cron models.Cron
|
||||
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
}
|
||||
|
||||
if !cron.Status {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务已禁用")
|
||||
}
|
||||
|
||||
cron.Time = ctx.Request().Input("time")
|
||||
cron.Name = ctx.Request().Input("name")
|
||||
err = facades.Orm().Query().Save(&cron)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 更新计划任务失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if !tools.Write(cron.Shell, ctx.Request().Input("script"), 0644) {
|
||||
facades.Log().Error("[面板][CronController] 更新计划任务脚本失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
tools.Exec("dos2unix " + cron.Shell)
|
||||
|
||||
c.cron.DeleteFromSystem(cron)
|
||||
if cron.Status {
|
||||
c.cron.AddToSystem(cron)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除计划任务
|
||||
func (c *CronController) Delete(ctx http.Context) http.Response {
|
||||
var cron models.Cron
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
}
|
||||
|
||||
c.cron.DeleteFromSystem(cron)
|
||||
tools.Remove(cron.Shell)
|
||||
|
||||
_, err = facades.Orm().Query().Delete(&cron)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 删除计划任务失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Status 更新计划任务状态
|
||||
func (c *CronController) Status(ctx http.Context) http.Response {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"status": "bool",
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
var cron models.Cron
|
||||
err = facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
}
|
||||
|
||||
cron.Status = ctx.Request().InputBool("status")
|
||||
err = facades.Orm().Query().Save(&cron)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][CronController] 更新计划任务状态失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.cron.DeleteFromSystem(cron)
|
||||
if cron.Status {
|
||||
c.cron.AddToSystem(cron)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Log 获取计划任务日志
|
||||
func (c *CronController) Log(ctx http.Context) http.Response {
|
||||
var cron models.Cron
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&cron)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "计划任务不存在")
|
||||
}
|
||||
|
||||
if !tools.Exists(cron.Log) {
|
||||
return Error(ctx, http.StatusBadRequest, "日志文件不存在")
|
||||
}
|
||||
|
||||
return Success(ctx, tools.Read(cron.Log))
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package controllers
|
||||
|
||||
type FileController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewFileController() *FileController {
|
||||
return &FileController{
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
func Success(ctx http.Context, data any) http.Response {
|
||||
return ctx.Response().Success().Json(http.Json{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": data,
|
||||
})
|
||||
}
|
||||
|
||||
func Error(ctx http.Context, code int, message any) http.Response {
|
||||
return ctx.Response().Json(http.StatusOK, http.Json{
|
||||
"code": code,
|
||||
"message": message,
|
||||
})
|
||||
}
|
||||
|
||||
// Check 检查插件是否可用
|
||||
func Check(ctx http.Context, slug string) http.Response {
|
||||
plugin := services.NewPluginImpl().GetBySlug(slug)
|
||||
installedPlugin := services.NewPluginImpl().GetInstalledBySlug(slug)
|
||||
installedPlugins, err := services.NewPluginImpl().AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][插件] 获取已安装插件失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if installedPlugin.Version != plugin.Version || installedPlugin.Slug != plugin.Slug {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要更新至 "+plugin.Version+" 版本")
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type MenuItem struct {
|
||||
Name string `json:"name"`
|
||||
Title string `json:"title"`
|
||||
Icon string `json:"icon"`
|
||||
Jump string `json:"jump"`
|
||||
}
|
||||
|
||||
type InfoController struct {
|
||||
plugin services.Plugin
|
||||
setting services.Setting
|
||||
}
|
||||
|
||||
func NewInfoController() *InfoController {
|
||||
return &InfoController{
|
||||
plugin: services.NewPluginImpl(),
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Name 获取面板名称
|
||||
func (c *InfoController) Name(ctx http.Context) http.Response {
|
||||
var setting models.Setting
|
||||
err := facades.Orm().Query().Where("key", "name").First(&setting)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][InfoController] 查询面板名称失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"name": setting.Value,
|
||||
})
|
||||
}
|
||||
|
||||
// HomePlugins 获取首页插件
|
||||
func (c *InfoController) HomePlugins(ctx http.Context) http.Response {
|
||||
var plugins []models.Plugin
|
||||
err := facades.Orm().Query().Where("show", 1).Find(&plugins)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][InfoController] 查询首页插件失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
type pluginsData struct {
|
||||
models.Plugin
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var pluginsJson []pluginsData
|
||||
for _, plugin := range plugins {
|
||||
pluginsJson = append(pluginsJson, pluginsData{
|
||||
Plugin: plugin,
|
||||
Name: services.NewPluginImpl().GetBySlug(plugin.Slug).Name,
|
||||
})
|
||||
}
|
||||
|
||||
return Success(ctx, pluginsJson)
|
||||
}
|
||||
|
||||
// NowMonitor 获取当前监控信息
|
||||
func (c *InfoController) NowMonitor(ctx http.Context) http.Response {
|
||||
return Success(ctx, tools.GetMonitoringInfo())
|
||||
}
|
||||
|
||||
// SystemInfo 获取系统信息
|
||||
func (c *InfoController) SystemInfo(ctx http.Context) http.Response {
|
||||
monitorInfo := tools.GetMonitoringInfo()
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"os_name": monitorInfo.Host.Platform + " " + monitorInfo.Host.PlatformVersion,
|
||||
"uptime": fmt.Sprintf("%.2f", float64(monitorInfo.Host.Uptime)/86400),
|
||||
"panel_version": facades.Config().GetString("panel.version"),
|
||||
})
|
||||
}
|
||||
|
||||
// CountInfo 获取面板统计信息
|
||||
func (c *InfoController) CountInfo(ctx http.Context) http.Response {
|
||||
var websiteCount int64
|
||||
err := facades.Orm().Query().Model(models.Website{}).Count(&websiteCount)
|
||||
if err != nil {
|
||||
websiteCount = -1
|
||||
}
|
||||
|
||||
var mysql models.Plugin
|
||||
mysqlInstalled := true
|
||||
err = facades.Orm().Query().Where("slug like ?", "mysql%").FirstOrFail(&mysql)
|
||||
if err != nil {
|
||||
mysqlInstalled = false
|
||||
}
|
||||
var postgresql models.Plugin
|
||||
postgresqlInstalled := true
|
||||
err = facades.Orm().Query().Where("slug like ?", "postgresql%").FirstOrFail(&postgresql)
|
||||
if err != nil {
|
||||
postgresqlInstalled = false
|
||||
}
|
||||
var databaseCount int64
|
||||
if mysqlInstalled {
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status == "active" {
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
type database struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("[面板][InfoController] 获取数据库列表失败")
|
||||
databaseCount = -1
|
||||
} else {
|
||||
defer db.Close()
|
||||
rows, err := db.Query("SHOW DATABASES")
|
||||
if err != nil {
|
||||
facades.Log().With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("[面板][InfoController] 获取数据库列表失败")
|
||||
databaseCount = -1
|
||||
} else {
|
||||
defer rows.Close()
|
||||
var databases []database
|
||||
for rows.Next() {
|
||||
var d database
|
||||
err := rows.Scan(&d.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if d.Name == "information_schema" || d.Name == "performance_schema" || d.Name == "mysql" || d.Name == "sys" {
|
||||
continue
|
||||
}
|
||||
|
||||
databases = append(databases, d)
|
||||
}
|
||||
databaseCount = int64(len(databases))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if postgresqlInstalled {
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status == "active" {
|
||||
raw := tools.Exec(`echo "\l" | su - postgres -c "psql"`)
|
||||
databases := strings.Split(raw, "\n")
|
||||
databases = databases[3 : len(databases)-1]
|
||||
for _, db := range databases {
|
||||
parts := strings.Split(db, "|")
|
||||
if len(parts) != 9 || len(strings.TrimSpace(parts[0])) == 0 || strings.TrimSpace(parts[0]) == "template0" || strings.TrimSpace(parts[0]) == "template1" || strings.TrimSpace(parts[0]) == "postgres" {
|
||||
continue
|
||||
}
|
||||
|
||||
databaseCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ftpCount int64
|
||||
var ftpPlugin = c.plugin.GetInstalledBySlug("pureftpd")
|
||||
if ftpPlugin.ID != 0 {
|
||||
listRaw := tools.Exec("pure-pw list")
|
||||
if len(listRaw) != 0 {
|
||||
listArr := strings.Split(listRaw, "\n")
|
||||
ftpCount = int64(len(listArr))
|
||||
}
|
||||
}
|
||||
|
||||
var cronCount int64
|
||||
err = facades.Orm().Query().Model(models.Cron{}).Count(&cronCount)
|
||||
if err != nil {
|
||||
cronCount = -1
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"website": websiteCount,
|
||||
"database": databaseCount,
|
||||
"ftp": ftpCount,
|
||||
"cron": cronCount,
|
||||
})
|
||||
}
|
||||
|
||||
// InstalledDbAndPhp 获取已安装的数据库和 PHP 版本
|
||||
func (c *InfoController) InstalledDbAndPhp(ctx http.Context) http.Response {
|
||||
var php []models.Plugin
|
||||
err := facades.Orm().Query().Where("slug like ?", "php%").Find(&php)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
var mysql models.Plugin
|
||||
mysqlInstalled := true
|
||||
err = facades.Orm().Query().Where("slug like ?", "mysql%").FirstOrFail(&mysql)
|
||||
if err != nil {
|
||||
mysqlInstalled = false
|
||||
}
|
||||
|
||||
var postgresql models.Plugin
|
||||
postgresqlInstalled := true
|
||||
err = facades.Orm().Query().Where("slug like ?", "postgresql%").FirstOrFail(&postgresql)
|
||||
if err != nil {
|
||||
postgresqlInstalled = false
|
||||
}
|
||||
|
||||
type data struct {
|
||||
Label string `json:"label"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
var phpData []data
|
||||
var dbData []data
|
||||
phpData = append(phpData, data{Value: "0", Label: "不使用"})
|
||||
dbData = append(dbData, data{Value: "0", Label: "不使用"})
|
||||
for _, p := range php {
|
||||
match := regexp.MustCompile(`php(\d+)`).FindStringSubmatch(p.Slug)
|
||||
if len(match) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
phpData = append(phpData, data{Value: strings.ReplaceAll(p.Slug, "php", ""), Label: c.plugin.GetBySlug(p.Slug).Name})
|
||||
}
|
||||
|
||||
if mysqlInstalled {
|
||||
dbData = append(dbData, data{Value: "mysql", Label: "MySQL"})
|
||||
}
|
||||
if postgresqlInstalled {
|
||||
dbData = append(dbData, data{Value: "postgresql", Label: "PostgreSQL"})
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"php": phpData,
|
||||
"db": dbData,
|
||||
})
|
||||
}
|
||||
|
||||
// CheckUpdate 检查面板更新
|
||||
func (c *InfoController) CheckUpdate(ctx http.Context) http.Response {
|
||||
version := facades.Config().GetString("panel.version")
|
||||
remote, err := tools.GetLatestPanelVersion()
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
|
||||
}
|
||||
|
||||
if tools.VersionCompare(version, remote.Version, ">=") {
|
||||
return Success(ctx, http.Json{
|
||||
"update": false,
|
||||
})
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"update": true,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateInfo 获取更新信息
|
||||
func (c *InfoController) UpdateInfo(ctx http.Context) http.Response {
|
||||
version := facades.Config().GetString("panel.version")
|
||||
current, err := tools.GetLatestPanelVersion()
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
|
||||
}
|
||||
|
||||
if tools.VersionCompare(version, current.Version, ">=") {
|
||||
return Error(ctx, http.StatusInternalServerError, "当前版本已是最新版本")
|
||||
}
|
||||
|
||||
versions, err := tools.GenerateVersions(version, current.Version)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "获取更新信息失败")
|
||||
}
|
||||
|
||||
var versionInfo []tools.PanelInfo
|
||||
for _, v := range versions {
|
||||
info, err := tools.GetPanelVersion(v)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
versionInfo = append(versionInfo, info)
|
||||
}
|
||||
|
||||
return Success(ctx, versionInfo)
|
||||
}
|
||||
|
||||
// Update 更新面板
|
||||
func (c *InfoController) Update(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止更新")
|
||||
}
|
||||
|
||||
panel, err := tools.GetLatestPanelVersion()
|
||||
if err != nil {
|
||||
facades.Log().With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("[面板][InfoController] 获取最新版本失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "获取最新版本失败")
|
||||
}
|
||||
|
||||
err = tools.UpdatePanel(panel)
|
||||
if err != nil {
|
||||
facades.Log().With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("[面板][InfoController] 更新面板失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "更新失败: "+err.Error())
|
||||
}
|
||||
|
||||
tools.RestartPanel()
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Restart 重启面板
|
||||
func (c *InfoController) Restart(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusRunning).OrWhere("status", models.TaskStatusWaiting).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "当前有任务正在执行,禁止重启")
|
||||
}
|
||||
|
||||
tools.RestartPanel()
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
type MonitorController struct {
|
||||
setting services.Setting
|
||||
}
|
||||
|
||||
func NewMonitorController() *MonitorController {
|
||||
return &MonitorController{
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Switch 监控开关
|
||||
func (r *MonitorController) Switch(ctx http.Context) http.Response {
|
||||
value := ctx.Request().InputBool("monitor")
|
||||
err := r.setting.Set(models.SettingKeyMonitor, cast.ToString(value))
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][MonitorController] 更新监控开关失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SaveDays 保存监控天数
|
||||
func (r *MonitorController) SaveDays(ctx http.Context) http.Response {
|
||||
days := ctx.Request().Input("days")
|
||||
err := r.setting.Set(models.SettingKeyMonitorDays, days)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][MonitorController] 更新监控天数失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SwitchAndDays 监控开关和监控天数
|
||||
func (r *MonitorController) SwitchAndDays(ctx http.Context) http.Response {
|
||||
monitor := r.setting.Get(models.SettingKeyMonitor)
|
||||
monitorDays := r.setting.Get(models.SettingKeyMonitorDays)
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"switch": cast.ToBool(monitor),
|
||||
"days": cast.ToInt(monitorDays),
|
||||
})
|
||||
}
|
||||
|
||||
// Clear 清空监控数据
|
||||
func (r *MonitorController) Clear(ctx http.Context) http.Response {
|
||||
_, err := facades.Orm().Query().Where("1 = 1").Delete(&models.Monitor{})
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][MonitorController] 清空监控数据失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// List 监控数据列表
|
||||
func (r *MonitorController) List(ctx http.Context) http.Response {
|
||||
start := ctx.Request().InputInt64("start")
|
||||
end := ctx.Request().InputInt64("end")
|
||||
startTime := carbon.FromTimestampMilli(start)
|
||||
endTime := carbon.FromTimestampMilli(end)
|
||||
|
||||
var monitors []models.Monitor
|
||||
err := facades.Orm().Query().Where("created_at >= ?", startTime.ToDateTimeString()).Where("created_at <= ?", endTime.ToDateTimeString()).Get(&monitors)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][MonitorController] 查询监控数据失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if len(monitors) == 0 {
|
||||
return Error(ctx, http.StatusNotFound, "监控数据为空")
|
||||
}
|
||||
|
||||
type load struct {
|
||||
Load1 []float64 `json:"load1"`
|
||||
Load5 []float64 `json:"load5"`
|
||||
Load15 []float64 `json:"load15"`
|
||||
}
|
||||
type cpu struct {
|
||||
Percent []string `json:"percent"`
|
||||
}
|
||||
type mem struct {
|
||||
Total string `json:"total"`
|
||||
Available []string `json:"available"`
|
||||
Used []string `json:"used"`
|
||||
}
|
||||
type swap struct {
|
||||
Total string `json:"total"`
|
||||
Used []string `json:"used"`
|
||||
Free []string `json:"free"`
|
||||
}
|
||||
type network struct {
|
||||
Sent []string `json:"sent"`
|
||||
Recv []string `json:"recv"`
|
||||
Tx []string `json:"tx"`
|
||||
Rx []string `json:"rx"`
|
||||
}
|
||||
type monitorData struct {
|
||||
Times []string `json:"times"`
|
||||
Load load `json:"load"`
|
||||
Cpu cpu `json:"cpu"`
|
||||
Mem mem `json:"mem"`
|
||||
Swap swap `json:"swap"`
|
||||
Net network `json:"net"`
|
||||
}
|
||||
|
||||
var data monitorData
|
||||
var bytesSent uint64
|
||||
var bytesRecv uint64
|
||||
var bytesSent2 uint64
|
||||
var bytesRecv2 uint64
|
||||
for _, net := range monitors[0].Info.Net {
|
||||
if net.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
bytesSent += net.BytesSent
|
||||
bytesRecv += net.BytesRecv
|
||||
}
|
||||
for i, monitor := range monitors {
|
||||
// 跳过第一条数据,因为第一条数据的流量为 0
|
||||
if i == 0 {
|
||||
// MB
|
||||
data.Mem.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Total)/1024/1024)
|
||||
data.Swap.Total = fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Total)/1024/1024)
|
||||
continue
|
||||
}
|
||||
for _, net := range monitor.Info.Net {
|
||||
if net.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
bytesSent2 += net.BytesSent
|
||||
bytesRecv2 += net.BytesRecv
|
||||
}
|
||||
data.Times = append(data.Times, monitor.CreatedAt.ToDateTimeString())
|
||||
data.Load.Load1 = append(data.Load.Load1, monitor.Info.Load.Load1)
|
||||
data.Load.Load5 = append(data.Load.Load5, monitor.Info.Load.Load5)
|
||||
data.Load.Load15 = append(data.Load.Load15, monitor.Info.Load.Load15)
|
||||
data.Cpu.Percent = append(data.Cpu.Percent, fmt.Sprintf("%.2f", monitor.Info.Percent[0]))
|
||||
data.Mem.Available = append(data.Mem.Available, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Available)/1024/1024))
|
||||
data.Mem.Used = append(data.Mem.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Mem.Used)/1024/1024))
|
||||
data.Swap.Used = append(data.Swap.Used, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Used)/1024/1024))
|
||||
data.Swap.Free = append(data.Swap.Free, fmt.Sprintf("%.2f", float64(monitor.Info.Swap.Free)/1024/1024))
|
||||
data.Net.Sent = append(data.Net.Sent, fmt.Sprintf("%.2f", float64(bytesSent2/1024/1024)))
|
||||
data.Net.Recv = append(data.Net.Recv, fmt.Sprintf("%.2f", float64(bytesRecv2/1024/1024)))
|
||||
|
||||
// 监控频率为 1 分钟,所以这里除以 60 即可得到每秒的流量
|
||||
data.Net.Tx = append(data.Net.Tx, fmt.Sprintf("%.2f", float64(bytesSent2-bytesSent)/60/1024/1024))
|
||||
data.Net.Rx = append(data.Net.Rx, fmt.Sprintf("%.2f", float64(bytesRecv2-bytesRecv)/60/1024/1024))
|
||||
|
||||
bytesSent = bytesSent2
|
||||
bytesRecv = bytesRecv2
|
||||
bytesSent2 = 0
|
||||
bytesRecv2 = 0
|
||||
}
|
||||
|
||||
return Success(ctx, data)
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
type PluginController struct {
|
||||
plugin services.Plugin
|
||||
task services.Task
|
||||
}
|
||||
|
||||
func NewPluginController() *PluginController {
|
||||
return &PluginController{
|
||||
plugin: services.NewPluginImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 列出所有插件
|
||||
func (r *PluginController) List(ctx http.Context) http.Response {
|
||||
plugins := r.plugin.All()
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
installedPluginsMap := make(map[string]models.Plugin)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
installedPluginsMap[p.Slug] = p
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug"`
|
||||
Version string `json:"version"`
|
||||
Requires []string `json:"requires"`
|
||||
Excludes []string `json:"excludes"`
|
||||
Installed bool `json:"installed"`
|
||||
InstalledVersion string `json:"installed_version"`
|
||||
Show bool `json:"show"`
|
||||
}
|
||||
|
||||
var pluginArr []plugin
|
||||
for _, item := range plugins {
|
||||
installed, installedVersion, show := false, "", false
|
||||
if _, ok := installedPluginsMap[item.Slug]; ok {
|
||||
installed = true
|
||||
installedVersion = installedPluginsMap[item.Slug].Version
|
||||
show = installedPluginsMap[item.Slug].Show
|
||||
}
|
||||
pluginArr = append(pluginArr, plugin{
|
||||
Name: item.Name,
|
||||
Description: item.Description,
|
||||
Slug: item.Slug,
|
||||
Version: item.Version,
|
||||
Requires: item.Requires,
|
||||
Excludes: item.Excludes,
|
||||
Installed: installed,
|
||||
InstalledVersion: installedVersion,
|
||||
Show: show,
|
||||
})
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(pluginArr) {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []plugin{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(pluginArr) {
|
||||
endIndex = len(pluginArr)
|
||||
}
|
||||
pagedPlugins := pluginArr[startIndex:endIndex]
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": len(pluginArr),
|
||||
"items": pagedPlugins,
|
||||
})
|
||||
}
|
||||
|
||||
// Install 安装插件
|
||||
func (r *PluginController) Install(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
plugin := r.plugin.GetBySlug(slug)
|
||||
installedPlugin := r.plugin.GetInstalledBySlug(slug)
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if installedPlugin.ID != 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "插件已安装")
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "安装插件 " + plugin.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = plugin.Install + " >> /tmp/" + plugin.Slug + ".log 2>&1"
|
||||
task.Log = "/tmp/" + plugin.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error())
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
// Uninstall 卸载插件
|
||||
func (r *PluginController) Uninstall(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
plugin := r.plugin.GetBySlug(slug)
|
||||
installedPlugin := r.plugin.GetInstalledBySlug(slug)
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if installedPlugin.ID == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "插件未安装")
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "卸载插件 " + plugin.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = plugin.Uninstall + " >> /tmp/" + plugin.Slug + ".log 2>&1"
|
||||
task.Log = "/tmp/" + plugin.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error())
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
// Update 更新插件
|
||||
func (r *PluginController) Update(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
plugin := r.plugin.GetBySlug(slug)
|
||||
installedPlugin := r.plugin.GetInstalledBySlug(slug)
|
||||
installedPlugins, err := r.plugin.AllInstalled()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 获取已安装插件失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if installedPlugin.ID == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "插件未安装")
|
||||
}
|
||||
|
||||
var lock sync.RWMutex
|
||||
pluginsMap := make(map[string]bool)
|
||||
|
||||
for _, p := range installedPlugins {
|
||||
lock.Lock()
|
||||
pluginsMap[p.Slug] = true
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
for _, require := range plugin.Requires {
|
||||
lock.RLock()
|
||||
_, requireFound := pluginsMap[require]
|
||||
lock.RUnlock()
|
||||
if !requireFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 需要依赖 "+require+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range plugin.Excludes {
|
||||
lock.RLock()
|
||||
_, excludeFound := pluginsMap[exclude]
|
||||
lock.RUnlock()
|
||||
if excludeFound {
|
||||
return Error(ctx, http.StatusForbidden, "插件 "+slug+" 不兼容 "+exclude+" 插件")
|
||||
}
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "更新插件 " + plugin.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = plugin.Update + " >> /tmp/" + plugin.Slug + ".log 2>&1"
|
||||
task.Log = "/tmp/" + plugin.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 创建任务失败: " + err.Error())
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
r.task.Process(task.ID)
|
||||
return Success(ctx, "任务已提交")
|
||||
}
|
||||
|
||||
// UpdateShow 更新插件首页显示状态
|
||||
func (r *PluginController) UpdateShow(ctx http.Context) http.Response {
|
||||
slug := ctx.Request().Input("slug")
|
||||
show := ctx.Request().InputBool("show")
|
||||
|
||||
var plugin models.Plugin
|
||||
if err := facades.Orm().Query().Where("slug", slug).First(&plugin); err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 查询插件失败: " + err.Error())
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
if plugin.ID == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "插件未安装")
|
||||
}
|
||||
|
||||
plugin.Show = show
|
||||
if err := facades.Orm().Query().Save(&plugin); err != nil {
|
||||
facades.Log().Error("[面板][PluginController] 更新插件失败: " + err.Error())
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, "操作成功")
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
package fail2ban
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Fail2banController struct {
|
||||
website services.Website
|
||||
}
|
||||
|
||||
func NewFail2banController() *Fail2banController {
|
||||
return &Fail2banController{
|
||||
website: services.NewWebsiteImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
type Jail struct {
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
LogPath string `json:"log_path"`
|
||||
MaxRetry int `json:"max_retry"`
|
||||
FindTime int `json:"find_time"`
|
||||
BanTime int `json:"ban_time"`
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *Fail2banController) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Fail2banController) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Fail2banController) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Fail2banController) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Fail2banController) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop fail2ban")
|
||||
status := tools.Exec("systemctl status fail2ban | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取服务运行状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// List 所有 Fail2ban 规则
|
||||
func (c *Fail2banController) List(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
if len(raw) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "Fail2ban 规则为空")
|
||||
}
|
||||
|
||||
jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1)
|
||||
if len(jailList) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "Fail2ban 规则为空")
|
||||
}
|
||||
|
||||
var jails []Jail
|
||||
for i, jail := range jailList {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
jailName := jail[1]
|
||||
jailRaw := tools.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
|
||||
if len(jailRaw) == 0 {
|
||||
continue
|
||||
}
|
||||
jailEnabled := strings.Contains(jailRaw, "enabled = true")
|
||||
jailLogPath := regexp.MustCompile(`logpath = (.*)`).FindStringSubmatch(jailRaw)
|
||||
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,
|
||||
LogPath: jailLogPath[1],
|
||||
MaxRetry: cast.ToInt(jailMaxRetry[1]),
|
||||
FindTime: cast.ToInt(jailFindTime[1]),
|
||||
BanTime: cast.ToInt(jailBanTime[1]),
|
||||
})
|
||||
}
|
||||
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(jails) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []Jail{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(jails) {
|
||||
endIndex = len(jails)
|
||||
}
|
||||
pagedJails := jails[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(jails),
|
||||
"items": pagedJails,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加 Fail2ban 规则
|
||||
func (c *Fail2banController) Add(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required",
|
||||
"type": "required|in:website,service",
|
||||
"maxretry": "required",
|
||||
"findtime": "required",
|
||||
"bantime": "required",
|
||||
"website_mode": "required_if:type,website",
|
||||
"website_path": "required_if:type,website",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
|
||||
}
|
||||
|
||||
jailName := ctx.Request().Input("name")
|
||||
jailType := ctx.Request().Input("type")
|
||||
jailMaxRetry := ctx.Request().Input("maxretry")
|
||||
jailFindTime := ctx.Request().Input("findtime")
|
||||
jailBanTime := ctx.Request().Input("bantime")
|
||||
jailWebsiteMode := ctx.Request().Input("website_mode")
|
||||
jailWebsitePath := ctx.Request().Input("website_path")
|
||||
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
if strings.Contains(raw, "["+jailName+"]") || (strings.Contains(raw, "["+jailName+"]"+"-cc") && jailWebsiteMode == "cc") || (strings.Contains(raw, "["+jailName+"]"+"-path") && jailWebsiteMode == "path") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "规则已存在")
|
||||
}
|
||||
|
||||
switch jailType {
|
||||
case "website":
|
||||
var website models.Website
|
||||
err := facades.Orm().Query().Where("name", jailName).FirstOrFail(&website)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "网站不存在")
|
||||
}
|
||||
config, err := c.website.GetConfig(int(website.ID))
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取网站配置失败")
|
||||
}
|
||||
var ports string
|
||||
for _, port := range config.Ports {
|
||||
if len(strings.Split(port, " ")) > 1 {
|
||||
ports += strings.Split(port, " ")[0] + ","
|
||||
} else {
|
||||
ports += port + ","
|
||||
}
|
||||
}
|
||||
|
||||
rule := `
|
||||
# ` + jailName + `-` + jailWebsiteMode + `-START
|
||||
[` + jailName + `-` + jailWebsiteMode + `]
|
||||
enabled = true
|
||||
filter = haozi-` + jailName + `-` + jailWebsiteMode + `
|
||||
port = ` + ports + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
action = %(action_mwl)s
|
||||
logpath = /www/wwwlogs/` + website.Name + `.log
|
||||
# ` + jailName + `-` + jailWebsiteMode + `-END
|
||||
`
|
||||
raw += rule
|
||||
tools.Write("/etc/fail2ban/jail.local", raw, 0644)
|
||||
|
||||
var filter string
|
||||
if jailWebsiteMode == "cc" {
|
||||
filter = `
|
||||
[Definition]
|
||||
failregex = ^<HOST>\s-.*HTTP/.*$
|
||||
ignoreregex =
|
||||
`
|
||||
} else {
|
||||
filter = `
|
||||
[Definition]
|
||||
failregex = ^<HOST>\s-.*\s` + jailWebsitePath + `.*HTTP/.*$
|
||||
ignoreregex =
|
||||
`
|
||||
}
|
||||
tools.Write("/etc/fail2ban/filter.d/haozi-"+jailName+"-"+jailWebsiteMode+".conf", filter, 0644)
|
||||
|
||||
case "service":
|
||||
var logPath string
|
||||
var filter string
|
||||
var port string
|
||||
switch jailName {
|
||||
case "ssh":
|
||||
if tools.IsDebian() {
|
||||
logPath = "/var/log/auth.log"
|
||||
} else {
|
||||
logPath = "/var/log/secure"
|
||||
}
|
||||
filter = "sshd"
|
||||
port = tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
case "mysql":
|
||||
logPath = "/www/server/mysql/mysql-error.log"
|
||||
filter = "mysqld-auth"
|
||||
port = tools.Exec("cat /www/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'")
|
||||
case "pure-ftpd":
|
||||
logPath = "/var/log/messages"
|
||||
filter = "pure-ftpd"
|
||||
port = tools.Exec(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
|
||||
default:
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "未知服务")
|
||||
}
|
||||
if len(port) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "获取服务端口失败,请检查是否安装")
|
||||
}
|
||||
|
||||
rule := `
|
||||
# ` + jailName + `-START
|
||||
[` + jailName + `]
|
||||
enabled = true
|
||||
filter = ` + filter + `
|
||||
port = ` + port + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
action = %(action_mwl)s
|
||||
logpath = ` + logPath + `
|
||||
# ` + jailName + `-END
|
||||
`
|
||||
raw += rule
|
||||
tools.Write("/etc/fail2ban/jail.local", raw, 0644)
|
||||
}
|
||||
|
||||
tools.Exec("fail2ban-client reload")
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除规则
|
||||
func (c *Fail2banController) Delete(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
jailName := ctx.Request().Input("name")
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
if !strings.Contains(raw, "["+jailName+"]") {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "规则不存在")
|
||||
}
|
||||
|
||||
rule := tools.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
|
||||
raw = strings.Replace(raw, "\n# "+jailName+"-START"+rule+"# "+jailName+"-END", "", -1)
|
||||
raw = strings.TrimSpace(raw)
|
||||
tools.Write("/etc/fail2ban/jail.local", raw, 0644)
|
||||
|
||||
tools.Exec("fail2ban-client reload")
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BanList 获取封禁列表
|
||||
func (c *Fail2banController) BanList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
name := ctx.Request().Query("name")
|
||||
if len(name) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
}
|
||||
|
||||
currentlyBan := tools.Exec(`fail2ban-client status ` + name + ` | grep "Currently banned" | awk '{print $4}'`)
|
||||
totalBan := tools.Exec(`fail2ban-client status ` + name + ` | grep "Total banned" | awk '{print $4}'`)
|
||||
bannedIp := tools.Exec(`fail2ban-client status ` + name + ` | grep "Banned IP list" | awk -F ":" '{print $2}'`)
|
||||
bannedIpList := strings.Split(bannedIp, " ")
|
||||
|
||||
var list []map[string]string
|
||||
for _, ip := range bannedIpList {
|
||||
if len(ip) > 0 {
|
||||
list = append(list, map[string]string{
|
||||
"name": name,
|
||||
"ip": ip,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"currentlyBan": currentlyBan,
|
||||
"totalBan": totalBan,
|
||||
"bannedIpList": list,
|
||||
})
|
||||
}
|
||||
|
||||
// Unban 解封
|
||||
func (c *Fail2banController) Unban(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
name := ctx.Request().Input("name")
|
||||
ip := ctx.Request().Input("ip")
|
||||
if len(name) == 0 || len(ip) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
}
|
||||
|
||||
tools.Exec("fail2ban-client set " + name + " unbanip " + ip)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SetWhiteList 设置白名单
|
||||
func (c *Fail2banController) SetWhiteList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
ip := ctx.Request().Input("ip")
|
||||
if len(ip) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "缺少参数")
|
||||
}
|
||||
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
// 正则替换
|
||||
reg := regexp.MustCompile(`ignoreip\s*=\s*.*\n`)
|
||||
if reg.MatchString(raw) {
|
||||
raw = reg.ReplaceAllString(raw, "ignoreip = "+ip+"\n")
|
||||
} else {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
|
||||
}
|
||||
|
||||
tools.Write("/etc/fail2ban/jail.local", raw, 0644)
|
||||
tools.Exec("fail2ban-client reload")
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetWhiteList 获取白名单
|
||||
func (c *Fail2banController) GetWhiteList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "fail2ban")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
raw := tools.Read("/etc/fail2ban/jail.local")
|
||||
reg := regexp.MustCompile(`ignoreip\s*=\s*(.*)\n`)
|
||||
if reg.MatchString(raw) {
|
||||
ignoreIp := reg.FindStringSubmatch(raw)[1]
|
||||
return controllers.Success(ctx, ignoreIp)
|
||||
} else {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "解析Fail2ban规则失败,Fail2ban可能已损坏")
|
||||
}
|
||||
}
|
||||
@@ -1,768 +0,0 @@
|
||||
package mysql57
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Mysql57Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
}
|
||||
|
||||
func NewMysql57Controller() *Mysql57Controller {
|
||||
return &Mysql57Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *Mysql57Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Mysql57Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Mysql57Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Mysql57Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Mysql57Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *Mysql57Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/mysql/conf/my.cnf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *Mysql57Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/mysql/conf/my.cnf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *Mysql57Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 已停止运行")
|
||||
}
|
||||
|
||||
raw := tools.Exec("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
|
||||
if strings.Contains(raw, "Access denied for user") {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码错误")
|
||||
}
|
||||
if !strings.Contains(raw, "Uptime") {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
|
||||
}
|
||||
|
||||
var data []map[string]string
|
||||
expressions := []struct {
|
||||
regex string
|
||||
name string
|
||||
}{
|
||||
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
|
||||
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
|
||||
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
|
||||
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
|
||||
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
|
||||
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
|
||||
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
|
||||
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
|
||||
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
|
||||
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
|
||||
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
|
||||
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
|
||||
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
|
||||
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
|
||||
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
|
||||
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
|
||||
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
|
||||
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
|
||||
}
|
||||
|
||||
for _, expression := range expressions {
|
||||
re := regexp.MustCompile(expression.regex)
|
||||
matches := re.FindStringSubmatch(raw)
|
||||
if len(matches) > 1 {
|
||||
d := make(map[string]string)
|
||||
d = map[string]string{"name": expression.name, "value": matches[1]}
|
||||
|
||||
if expression.name == "发送" || expression.name == "接收" {
|
||||
d["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
|
||||
}
|
||||
|
||||
data = append(data, d)
|
||||
}
|
||||
}
|
||||
|
||||
// 索引命中率
|
||||
readRequests := cast.ToFloat64(data[9]["value"])
|
||||
reads := cast.ToFloat64(data[10]["value"])
|
||||
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
|
||||
// Innodb 索引命中率
|
||||
bufferPoolReads := cast.ToFloat64(data[11]["value"])
|
||||
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
|
||||
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (c *Mysql57Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-error.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (c *Mysql57Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-error.log")
|
||||
return controllers.Success(ctx, "清空错误日志成功")
|
||||
}
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (c *Mysql57Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearSlowLog 清空慢查询日志
|
||||
func (c *Mysql57Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-slow.log")
|
||||
return controllers.Success(ctx, "清空慢查询日志成功")
|
||||
}
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
func (c *Mysql57Controller) GetRootPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, rootPassword)
|
||||
}
|
||||
|
||||
// SetRootPassword 设置root密码
|
||||
func (c *Mysql57Controller) SetRootPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
|
||||
}
|
||||
|
||||
rootPassword := ctx.Request().Input(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码不能为空")
|
||||
}
|
||||
|
||||
oldRootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if oldRootPassword != rootPassword {
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
err := c.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
|
||||
if err != nil {
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "设置root密码成功")
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (c *Mysql57Controller) DatabaseList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
type database struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL57] 连接数据库失败" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query("SHOW DATABASES")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL57] 获取数据库列表失败" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var databases []database
|
||||
for rows.Next() {
|
||||
var d database
|
||||
err := rows.Scan(&d.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
databases = append(databases, d)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
facades.Log().Error("[MySQL57] 获取数据库列表失败" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(databases) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(databases) {
|
||||
endIndex = len(databases)
|
||||
}
|
||||
pagedDatabases := databases[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(databases),
|
||||
"items": pagedDatabases,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (c *Mysql57Controller) AddDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
database := ctx.Request().Input("database")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
return controllers.Success(ctx, "添加数据库成功")
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (c *Mysql57Controller) DeleteDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"")
|
||||
|
||||
return controllers.Success(ctx, "删除数据库成功")
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (c *Mysql57Controller) BackupList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
backupList, err := c.backup.MysqlList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL57] 获取备份列表失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *Mysql57Controller) UploadBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
name := file.GetClientOriginalName()
|
||||
_, err = file.StoreAs(backupPath, name)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "上传文件成功")
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *Mysql57Controller) CreateBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
err = c.backup.MysqlBackup(database)
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL57] 创建备份失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "备份成功")
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *Mysql57Controller) DeleteBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"backup": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
fileName := ctx.Request().Input("backup")
|
||||
tools.Remove(backupPath + "/" + fileName)
|
||||
|
||||
return controllers.Success(ctx, "删除备份成功")
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *Mysql57Controller) RestoreBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"backup": "required|min_len:1|max_len:255",
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
err = c.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup"))
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL57] 还原失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "还原成功")
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (c *Mysql57Controller) UserList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
type user struct {
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
Grants []string `json:"grants"`
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL57] 连接数据库失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query("SELECT user, host FROM mysql.user")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL57] 查询数据库失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "查询数据库失败")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var userGrants []user
|
||||
|
||||
for rows.Next() {
|
||||
var u user
|
||||
err := rows.Scan(&u.User, &u.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 查询用户权限
|
||||
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer grantsRows.Close()
|
||||
|
||||
for grantsRows.Next() {
|
||||
var grant string
|
||||
err := grantsRows.Scan(&grant)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
u.Grants = append(u.Grants, grant)
|
||||
}
|
||||
|
||||
if err := grantsRows.Err(); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
userGrants = append(userGrants, u)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(userGrants) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []user{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(userGrants) {
|
||||
endIndex = len(userGrants)
|
||||
}
|
||||
pagedUserGrants := userGrants[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(userGrants),
|
||||
"items": pagedUserGrants,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (c *Mysql57Controller) AddUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
return controllers.Success(ctx, "添加成功")
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (c *Mysql57Controller) DeleteUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\"")
|
||||
|
||||
return controllers.Success(ctx, "删除成功")
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (c *Mysql57Controller) SetUserPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
return controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
|
||||
// SetUserPrivileges 设置用户权限
|
||||
func (c *Mysql57Controller) SetUserPrivileges(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql57")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"database": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
return controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
@@ -1,768 +0,0 @@
|
||||
package mysql80
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Mysql80Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
}
|
||||
|
||||
func NewMysql80Controller() *Mysql80Controller {
|
||||
return &Mysql80Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *Mysql80Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Mysql80Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Mysql80Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Mysql80Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Mysql80Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop mysqld")
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *Mysql80Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/mysql/conf/my.cnf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *Mysql80Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/mysql/conf/my.cnf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入MySQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *Mysql80Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 已停止运行")
|
||||
}
|
||||
|
||||
raw := tools.Exec("/www/server/mysql/bin/mysqladmin -uroot -p" + rootPassword + " extended-status 2>&1")
|
||||
if strings.Contains(raw, "Access denied for user") {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码错误")
|
||||
}
|
||||
if !strings.Contains(raw, "Uptime") {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL负载失败")
|
||||
}
|
||||
|
||||
var data []map[string]string
|
||||
expressions := []struct {
|
||||
regex string
|
||||
name string
|
||||
}{
|
||||
{`Uptime\s+\|\s+(\d+)\s+\|`, "运行时间"},
|
||||
{`Queries\s+\|\s+(\d+)\s+\|`, "总查询次数"},
|
||||
{`Connections\s+\|\s+(\d+)\s+\|`, "总连接次数"},
|
||||
{`Com_commit\s+\|\s+(\d+)\s+\|`, "每秒事务"},
|
||||
{`Com_rollback\s+\|\s+(\d+)\s+\|`, "每秒回滚"},
|
||||
{`Bytes_sent\s+\|\s+(\d+)\s+\|`, "发送"},
|
||||
{`Bytes_received\s+\|\s+(\d+)\s+\|`, "接收"},
|
||||
{`Threads_connected\s+\|\s+(\d+)\s+\|`, "活动连接数"},
|
||||
{`Max_used_connections\s+\|\s+(\d+)\s+\|`, "峰值连接数"},
|
||||
{`Key_read_requests\s+\|\s+(\d+)\s+\|`, "索引命中率"},
|
||||
{`Innodb_buffer_pool_reads\s+\|\s+(\d+)\s+\|`, "Innodb索引命中率"},
|
||||
{`Created_tmp_disk_tables\s+\|\s+(\d+)\s+\|`, "创建临时表到磁盘"},
|
||||
{`Open_tables\s+\|\s+(\d+)\s+\|`, "已打开的表"},
|
||||
{`Select_full_join\s+\|\s+(\d+)\s+\|`, "没有使用索引的量"},
|
||||
{`Select_full_range_join\s+\|\s+(\d+)\s+\|`, "没有索引的JOIN量"},
|
||||
{`Select_range_check\s+\|\s+(\d+)\s+\|`, "没有索引的子查询量"},
|
||||
{`Sort_merge_passes\s+\|\s+(\d+)\s+\|`, "排序后的合并次数"},
|
||||
{`Table_locks_waited\s+\|\s+(\d+)\s+\|`, "锁表次数"},
|
||||
}
|
||||
|
||||
for _, expression := range expressions {
|
||||
re := regexp.MustCompile(expression.regex)
|
||||
matches := re.FindStringSubmatch(raw)
|
||||
if len(matches) > 1 {
|
||||
d := make(map[string]string)
|
||||
d = map[string]string{"name": expression.name, "value": matches[1]}
|
||||
|
||||
if expression.name == "发送" || expression.name == "接收" {
|
||||
d["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
|
||||
}
|
||||
|
||||
data = append(data, d)
|
||||
}
|
||||
}
|
||||
|
||||
// 索引命中率
|
||||
readRequests := cast.ToFloat64(data[9]["value"])
|
||||
reads := cast.ToFloat64(data[10]["value"])
|
||||
data[9]["value"] = fmt.Sprintf("%.2f%%", readRequests/(reads+readRequests)*100)
|
||||
// Innodb 索引命中率
|
||||
bufferPoolReads := cast.ToFloat64(data[11]["value"])
|
||||
bufferPoolReadRequests := cast.ToFloat64(data[12]["value"])
|
||||
data[10]["value"] = fmt.Sprintf("%.2f%%", bufferPoolReadRequests/(bufferPoolReads+bufferPoolReadRequests)*100)
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (c *Mysql80Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-error.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (c *Mysql80Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-error.log")
|
||||
return controllers.Success(ctx, "清空错误日志成功")
|
||||
}
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (c *Mysql80Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/mysql/mysql-slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearSlowLog 清空慢查询日志
|
||||
func (c *Mysql80Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/mysql/mysql-slow.log")
|
||||
return controllers.Success(ctx, "清空慢查询日志成功")
|
||||
}
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
func (c *Mysql80Controller) GetRootPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码为空")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, rootPassword)
|
||||
}
|
||||
|
||||
// SetRootPassword 设置root密码
|
||||
func (c *Mysql80Controller) SetRootPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status mysqld | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取MySQL状态失败")
|
||||
}
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "MySQL 未运行")
|
||||
}
|
||||
|
||||
rootPassword := ctx.Request().Input(models.SettingKeyMysqlRootPassword)
|
||||
if len(rootPassword) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "MySQL root密码不能为空")
|
||||
}
|
||||
|
||||
oldRootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
if oldRootPassword != rootPassword {
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + rootPassword + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + oldRootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
err := c.setting.Set(models.SettingKeyMysqlRootPassword, rootPassword)
|
||||
if err != nil {
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER 'root'@'localhost' IDENTIFIED BY '" + oldRootPassword + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "设置root密码失败")
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "设置root密码成功")
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (c *Mysql80Controller) DatabaseList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
type database struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL80] 连接数据库失败" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query("SHOW DATABASES")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL80] 获取数据库列表失败" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var databases []database
|
||||
for rows.Next() {
|
||||
var d database
|
||||
err := rows.Scan(&d.Name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
databases = append(databases, d)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
facades.Log().Error("[MySQL80] 获取数据库列表失败" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取数据库列表失败")
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(databases) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(databases) {
|
||||
endIndex = len(databases)
|
||||
}
|
||||
pagedDatabases := databases[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(databases),
|
||||
"items": pagedDatabases,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (c *Mysql80Controller) AddDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
database := ctx.Request().Input("database")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE DATABASE IF NOT EXISTS " + database + " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
return controllers.Success(ctx, "添加数据库成功")
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (c *Mysql80Controller) DeleteDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP DATABASE IF EXISTS " + database + ";\"")
|
||||
|
||||
return controllers.Success(ctx, "删除数据库成功")
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (c *Mysql80Controller) BackupList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
backupList, err := c.backup.MysqlList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[MySQL80] 获取备份列表失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *Mysql80Controller) UploadBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
name := file.GetClientOriginalName()
|
||||
_, err = file.StoreAs(backupPath, name)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "上传文件成功")
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *Mysql80Controller) CreateBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
err = c.backup.MysqlBackup(database)
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL80] 创建备份失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "备份成功")
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *Mysql80Controller) DeleteBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"backup": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/mysql"
|
||||
fileName := ctx.Request().Input("backup")
|
||||
tools.Remove(backupPath + "/" + fileName)
|
||||
|
||||
return controllers.Success(ctx, "删除备份成功")
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *Mysql80Controller) RestoreBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"backup": "required|min_len:1|max_len:255",
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
err = c.backup.MysqlRestore(ctx.Request().Input("database"), ctx.Request().Input("backup"))
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL80] 还原失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, "还原成功")
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (c *Mysql80Controller) UserList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
type user struct {
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
Grants []string `json:"grants"`
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
db, err := sql.Open("mysql", "root:"+rootPassword+"@unix(/tmp/mysql.sock)/")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL80] 连接数据库失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "连接数据库失败")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query("SELECT user, host FROM mysql.user")
|
||||
if err != nil {
|
||||
facades.Log().Error("[MYSQL80] 查询数据库失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "查询数据库失败")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var userGrants []user
|
||||
|
||||
for rows.Next() {
|
||||
var u user
|
||||
err := rows.Scan(&u.User, &u.Host)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 查询用户权限
|
||||
grantsRows, err := db.Query(fmt.Sprintf("SHOW GRANTS FOR '%s'@'%s'", u.User, u.Host))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer grantsRows.Close()
|
||||
|
||||
for grantsRows.Next() {
|
||||
var grant string
|
||||
err := grantsRows.Scan(&grant)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
u.Grants = append(u.Grants, grant)
|
||||
}
|
||||
|
||||
if err := grantsRows.Err(); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
userGrants = append(userGrants, u)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取用户列表失败")
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(userGrants) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []user{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(userGrants) {
|
||||
endIndex = len(userGrants)
|
||||
}
|
||||
pagedUserGrants := userGrants[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(userGrants),
|
||||
"items": pagedUserGrants,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (c *Mysql80Controller) AddUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"CREATE USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + ";'\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
return controllers.Success(ctx, "添加成功")
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (c *Mysql80Controller) DeleteUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"DROP USER '" + user + "'@'localhost';\"")
|
||||
|
||||
return controllers.Success(ctx, "删除成功")
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (c *Mysql80Controller) SetUserPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"ALTER USER '" + user + "'@'localhost' IDENTIFIED BY '" + password + "';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
return controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
|
||||
// SetUserPrivileges 设置用户权限
|
||||
func (c *Mysql80Controller) SetUserPrivileges(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "mysql80")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"database": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
rootPassword := c.setting.Get(models.SettingKeyMysqlRootPassword)
|
||||
user := ctx.Request().Input("user")
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"REVOKE ALL PRIVILEGES ON *.* FROM '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"GRANT ALL PRIVILEGES ON " + database + ".* TO '" + user + "'@'localhost';\"")
|
||||
tools.Exec("/www/server/mysql/bin/mysql -uroot -p" + rootPassword + " -e \"FLUSH PRIVILEGES;\"")
|
||||
|
||||
return controllers.Success(ctx, "修改成功")
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
package openresty
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type OpenRestyController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewOpenrestyController() *OpenRestyController {
|
||||
return &OpenRestyController{
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *OpenRestyController) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *OpenRestyController) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, "重载OpenResty成功")
|
||||
} else {
|
||||
return controllers.Error(ctx, 1, "重载OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动OpenResty
|
||||
func (c *OpenRestyController) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, "启动OpenResty成功")
|
||||
} else {
|
||||
return controllers.Error(ctx, 1, "启动OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止OpenResty
|
||||
func (c *OpenRestyController) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, "停止OpenResty成功")
|
||||
} else {
|
||||
return controllers.Error(ctx, 1, "停止OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启OpenResty
|
||||
func (c *OpenRestyController) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart openresty")
|
||||
status := tools.Exec("systemctl status openresty | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, "重启OpenResty成功")
|
||||
} else {
|
||||
return controllers.Error(ctx, 1, "重启OpenResty失败: "+status)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *OpenRestyController) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/openresty/conf/nginx.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *OpenRestyController) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/openresty/conf/nginx.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "保存OpenResty配置失败")
|
||||
}
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (c *OpenRestyController) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
if !tools.Exists("/www/wwwlogs/nginx_error.log") {
|
||||
return controllers.Success(ctx, "")
|
||||
}
|
||||
|
||||
out := tools.Exec("tail -n 100 /www/wwwlogs/nginx_error.log")
|
||||
return controllers.Success(ctx, out)
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (c *OpenRestyController) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/wwwlogs/nginx_error.log")
|
||||
return controllers.Success(ctx, "清空OpenResty错误日志成功")
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *OpenRestyController) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/nginx_status")
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Error("[OpenResty] 获取OpenResty负载失败: " + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取OpenResty负载失败")
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
type nginxStatus struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
var data []nginxStatus
|
||||
|
||||
workers := tools.Exec("ps aux | grep nginx | grep 'worker process' | wc -l")
|
||||
data = append(data, nginxStatus{
|
||||
Name: "工作进程",
|
||||
Value: workers,
|
||||
})
|
||||
|
||||
out := tools.Exec("ps aux | grep nginx | grep 'worker process' | awk '{memsum+=$6};END {print memsum}'")
|
||||
mem := tools.FormatBytes(cast.ToFloat64(out))
|
||||
data = append(data, nginxStatus{
|
||||
Name: "内存占用",
|
||||
Value: mem,
|
||||
})
|
||||
|
||||
match := regexp.MustCompile(`Active connections:\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 2 {
|
||||
data = append(data, nginxStatus{
|
||||
Name: "活跃连接数",
|
||||
Value: match[1],
|
||||
})
|
||||
}
|
||||
|
||||
match = regexp.MustCompile(`server accepts handled requests\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 4 {
|
||||
data = append(data, nginxStatus{
|
||||
Name: "总连接次数",
|
||||
Value: match[1],
|
||||
})
|
||||
data = append(data, nginxStatus{
|
||||
Name: "总握手次数",
|
||||
Value: match[2],
|
||||
})
|
||||
data = append(data, nginxStatus{
|
||||
Name: "总请求次数",
|
||||
Value: match[3],
|
||||
})
|
||||
}
|
||||
|
||||
match = regexp.MustCompile(`Reading:\s+(\d+)\s+Writing:\s+(\d+)\s+Waiting:\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 4 {
|
||||
data = append(data, nginxStatus{
|
||||
Name: "请求数",
|
||||
Value: match[1],
|
||||
})
|
||||
data = append(data, nginxStatus{
|
||||
Name: "响应数",
|
||||
Value: match[2],
|
||||
})
|
||||
data = append(data, nginxStatus{
|
||||
Name: "驻留进程",
|
||||
Value: match[3],
|
||||
})
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
package php74
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Php74Controller struct {
|
||||
setting services.Setting
|
||||
task services.Task
|
||||
version string
|
||||
}
|
||||
|
||||
func NewPhp74Controller() *Php74Controller {
|
||||
return &Php74Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
version: "74",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php74Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/php/" + c.version + "/etc/php.ini")
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
tools.Write("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
|
||||
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
|
||||
|
||||
type Data struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
data := make([]Data, len(dataKeys))
|
||||
for i := range dataKeys {
|
||||
data[i].Name = dataKeys[i]
|
||||
|
||||
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
|
||||
match := r.FindStringSubmatch(raw)
|
||||
|
||||
if len(match) > 1 {
|
||||
data[i].Value = strings.TrimSpace(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
func (c *Php74Controller) GetExtensionList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (c *Php74Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php74Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php74Controller) GetExtensions() []Extension {
|
||||
var extensions []Extension
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "PhpRedis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "Exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的PHP加密解密工具。",
|
||||
Installed: false,
|
||||
})
|
||||
|
||||
raw := tools.Exec("/www/server/php/" + c.version + "/bin/php -m")
|
||||
rawExtensionList := strings.Split(raw, "\n")
|
||||
|
||||
for _, item := range rawExtensionList {
|
||||
if !strings.Contains(item, "[") && item != "" {
|
||||
for i := range extensions {
|
||||
if extensions[i].Slug == item {
|
||||
extensions[i].Installed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
package php80
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Php80Controller struct {
|
||||
setting services.Setting
|
||||
task services.Task
|
||||
version string
|
||||
}
|
||||
|
||||
func NewPhp80Controller() *Php80Controller {
|
||||
return &Php80Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
version: "80",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php80Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/php/" + c.version + "/etc/php.ini")
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
tools.Write("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
|
||||
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
|
||||
|
||||
type Data struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
data := make([]Data, len(dataKeys))
|
||||
for i := range dataKeys {
|
||||
data[i].Name = dataKeys[i]
|
||||
|
||||
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
|
||||
match := r.FindStringSubmatch(raw)
|
||||
|
||||
if len(match) > 1 {
|
||||
data[i].Value = strings.TrimSpace(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
func (c *Php80Controller) GetExtensionList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (c *Php80Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php80Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php80Controller) GetExtensions() []Extension {
|
||||
var extensions []Extension
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "PhpRedis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "Exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的PHP加密解密工具。",
|
||||
Installed: false,
|
||||
})
|
||||
|
||||
raw := tools.Exec("/www/server/php/" + c.version + "/bin/php -m")
|
||||
rawExtensionList := strings.Split(raw, "\n")
|
||||
|
||||
for _, item := range rawExtensionList {
|
||||
if !strings.Contains(item, "[") && item != "" {
|
||||
for i := range extensions {
|
||||
if extensions[i].Slug == item {
|
||||
extensions[i].Installed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
@@ -1,372 +0,0 @@
|
||||
package php81
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Php81Controller struct {
|
||||
setting services.Setting
|
||||
task services.Task
|
||||
version string
|
||||
}
|
||||
|
||||
func NewPhp81Controller() *Php81Controller {
|
||||
return &Php81Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
version: "81",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php81Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php81Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php81Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php81Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php81Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php81Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/php/" + c.version + "/etc/php.ini")
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
tools.Write("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
|
||||
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
|
||||
|
||||
type Data struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
data := make([]Data, len(dataKeys))
|
||||
for i := range dataKeys {
|
||||
data[i].Name = dataKeys[i]
|
||||
|
||||
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
|
||||
match := r.FindStringSubmatch(raw)
|
||||
|
||||
if len(match) > 1 {
|
||||
data[i].Value = strings.TrimSpace(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
func (c *Php81Controller) GetExtensionList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (c *Php81Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php81Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php81Controller) GetExtensions() []Extension {
|
||||
var extensions []Extension
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "PhpRedis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "Exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的PHP加密解密工具。",
|
||||
Installed: false,
|
||||
})
|
||||
|
||||
raw := tools.Exec("/www/server/php/" + c.version + "/bin/php -m")
|
||||
rawExtensionList := strings.Split(raw, "\n")
|
||||
|
||||
for _, item := range rawExtensionList {
|
||||
if !strings.Contains(item, "[") && item != "" {
|
||||
for i := range extensions {
|
||||
if extensions[i].Slug == item {
|
||||
extensions[i].Installed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
package php82
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/imroc/req/v3"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Php82Controller struct {
|
||||
setting services.Setting
|
||||
task services.Task
|
||||
version string
|
||||
}
|
||||
|
||||
func NewPhp82Controller() *Php82Controller {
|
||||
return &Php82Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
task: services.NewTaskImpl(),
|
||||
version: "82",
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php82Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php82Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php82Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php82Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php82Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart php-fpm-" + c.version)
|
||||
out := tools.Exec("systemctl status php-fpm-" + c.version + " | grep Active | grep -v grep | awk '{print $2}'")
|
||||
status := strings.TrimSpace(out)
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PHP-"+c.version+"运行状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Php82Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := tools.Read("/www/server/php/" + c.version + "/etc/php.ini")
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
tools.Write("/www/server/php/"+c.version+"/etc/php.ini", config, 0644)
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
client := req.C().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get("http://127.0.0.1/phpfpm_status/" + c.version)
|
||||
if err != nil || !resp.IsSuccessState() {
|
||||
facades.Log().Error("获取PHP-" + c.version + "运行状态失败")
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "[PHP-"+c.version+"] 获取运行状态失败")
|
||||
}
|
||||
|
||||
raw := resp.String()
|
||||
dataKeys := []string{"应用池", "工作模式", "启动时间", "接受连接", "监听队列", "最大监听队列", "监听队列长度", "空闲进程数量", "活动进程数量", "总进程数量", "最大活跃进程数量", "达到进程上限次数", "慢请求"}
|
||||
regexKeys := []string{"pool", "process manager", "start time", "accepted conn", "listen queue", "max listen queue", "listen queue len", "idle processes", "active processes", "total processes", "max active processes", "max children reached", "slow requests"}
|
||||
|
||||
type Data struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
data := make([]Data, len(dataKeys))
|
||||
for i := range dataKeys {
|
||||
data[i].Name = dataKeys[i]
|
||||
|
||||
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
|
||||
match := r.FindStringSubmatch(raw)
|
||||
|
||||
if len(match) > 1 {
|
||||
data[i].Value = strings.TrimSpace(match[1])
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) ErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/php-fpm.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) SlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Escape(tools.Exec("tail -n 100 /www/server/php/" + c.version + "/var/log/slow.log"))
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) ClearErrorLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/php-fpm.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) ClearSlowLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/php/" + c.version + "/var/log/slow.log")
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
func (c *Php82Controller) GetExtensionList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
return controllers.Success(ctx, extensions)
|
||||
}
|
||||
|
||||
func (c *Php82Controller) InstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展已安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "安装PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' install ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建安装拓展任务失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php82Controller) UninstallExtension(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "php"+c.version)
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
slug := ctx.Request().Input("slug")
|
||||
if len(slug) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
extensions := c.GetExtensions()
|
||||
for _, item := range extensions {
|
||||
if item.Slug == slug {
|
||||
if !item.Installed {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展未安装")
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
task.Name = "卸载PHP-" + c.version + "扩展-" + item.Name
|
||||
task.Status = models.TaskStatusWaiting
|
||||
task.Shell = `bash '/www/panel/scripts/php_extensions/` + item.Slug + `.sh' uninstall ` + c.version + ` >> /tmp/` + item.Slug + `.log 2>&1`
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err := facades.Orm().Query().Create(&task); err != nil {
|
||||
facades.Log().Error("[PHP-" + c.version + "] 创建卸载拓展任务失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
c.task.Process(task.ID)
|
||||
|
||||
return controllers.Success(ctx, true)
|
||||
}
|
||||
}
|
||||
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "扩展不存在")
|
||||
}
|
||||
|
||||
func (c *Php82Controller) GetExtensions() []Extension {
|
||||
var extensions []Extension
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "PhpRedis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用C语言编写的PHP模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "Exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Installed: false,
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
|
||||
Installed: false,
|
||||
})
|
||||
|
||||
raw := tools.Exec("/www/server/php/" + c.version + "/bin/php -m")
|
||||
rawExtensionList := strings.Split(raw, "\n")
|
||||
|
||||
for _, item := range rawExtensionList {
|
||||
if !strings.Contains(item, "[") && item != "" {
|
||||
for i := range extensions {
|
||||
if extensions[i].Slug == item {
|
||||
extensions[i].Installed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package phpmyadmin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type PhpMyAdminController struct {
|
||||
}
|
||||
|
||||
func NewPhpMyAdminController() *PhpMyAdminController {
|
||||
return &PhpMyAdminController{}
|
||||
}
|
||||
|
||||
func (c *PhpMyAdminController) Info(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "phpmyadmin")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
files, err := os.ReadDir("/www/server/phpmyadmin")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 目录")
|
||||
}
|
||||
|
||||
var phpmyadmin string
|
||||
for _, f := range files {
|
||||
if strings.HasPrefix(f.Name(), "phpmyadmin_") {
|
||||
phpmyadmin = f.Name()
|
||||
}
|
||||
}
|
||||
if len(phpmyadmin) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 目录")
|
||||
}
|
||||
|
||||
conf := tools.Read("/www/server/vhost/phpmyadmin.conf")
|
||||
match := regexp.MustCompile(`listen\s+(\d+);`).FindStringSubmatch(conf)
|
||||
if len(match) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "找不到 phpMyAdmin 端口")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"phpmyadmin": phpmyadmin,
|
||||
"port": match[1],
|
||||
})
|
||||
}
|
||||
|
||||
func (c *PhpMyAdminController) SetPort(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "phpmyadmin")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
port := ctx.Request().Input("port")
|
||||
if len(port) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "端口不能为空")
|
||||
}
|
||||
|
||||
conf := tools.Read("/www/server/vhost/phpmyadmin.conf")
|
||||
conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+port+";")
|
||||
tools.Write("/www/server/vhost/phpmyadmin.conf", conf, 0644)
|
||||
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("firewall-cmd --zone=public --add-port=" + port + "/tcp --permanent")
|
||||
tools.Exec("firewall-cmd --reload")
|
||||
} else {
|
||||
tools.Exec("ufw allow " + port + "/tcp")
|
||||
tools.Exec("ufw reload")
|
||||
}
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,612 +0,0 @@
|
||||
package postgresql15
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Postgresql15Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func NewPostgresql15Controller() *Postgresql15Controller {
|
||||
return &Postgresql15Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *Postgresql15Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Postgresql15Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Postgresql15Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Postgresql15Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Postgresql15Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *Postgresql15Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/postgresql/data/postgresql.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// GetUserConfig 获取用户配置
|
||||
func (c *Postgresql15Controller) GetUserConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/postgresql/data/pg_hba.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *Postgresql15Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/postgresql/data/postgresql.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// SaveUserConfig 保存用户配置
|
||||
func (c *Postgresql15Controller) SaveUserConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *Postgresql15Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL 已停止运行")
|
||||
}
|
||||
|
||||
data := []Info{
|
||||
{"启动时间", carbon.Parse(tools.Exec(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`)).ToDateTimeString()},
|
||||
{"进程 PID", tools.Exec(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)},
|
||||
{"进程数", tools.Exec(`ps aux | grep postgres | grep -v grep | wc -l`)},
|
||||
{"总连接数", tools.Exec(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)},
|
||||
{"空间占用", tools.Exec(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`)},
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// Log 获取日志
|
||||
func (c *Postgresql15Controller) Log(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Exec("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (c *Postgresql15Controller) ClearLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (c *Postgresql15Controller) DatabaseList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL 已停止运行")
|
||||
}
|
||||
|
||||
raw := tools.Exec(`echo "\l" | su - postgres -c "psql"`)
|
||||
databases := strings.Split(raw, "\n")
|
||||
databases = databases[3 : len(databases)-1]
|
||||
|
||||
type database struct {
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
var databaseList []database
|
||||
for _, db := range databases {
|
||||
parts := strings.Split(db, "|")
|
||||
if len(parts) != 8 || len(strings.TrimSpace(parts[0])) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
databaseList = append(databaseList, database{
|
||||
Name: strings.TrimSpace(parts[0]),
|
||||
Owner: strings.TrimSpace(parts[1]),
|
||||
Encoding: strings.TrimSpace(parts[2]),
|
||||
})
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(databaseList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(databaseList) {
|
||||
endIndex = len(databaseList)
|
||||
}
|
||||
pagedDatabases := databaseList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(databaseList),
|
||||
"items": pagedDatabases,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (c *Postgresql15Controller) AddDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
tools.Exec(`echo "CREATE DATABASE ` + database + `;" | su - postgres -c "psql"`)
|
||||
tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`)
|
||||
tools.Exec(`echo "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | su - postgres -c "psql"`)
|
||||
tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`)
|
||||
|
||||
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
|
||||
tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (c *Postgresql15Controller) DeleteDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (c *Postgresql15Controller) BackupList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
backupList, err := c.backup.PostgresqlList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 获取备份列表失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *Postgresql15Controller) UploadBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
name := file.GetClientOriginalName()
|
||||
_, err = file.StoreAs(backupPath, name)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *Postgresql15Controller) CreateBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
err = c.backup.PostgresqlBackup(database)
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 创建备份失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *Postgresql15Controller) DeleteBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
fileName := ctx.Request().Input("name")
|
||||
tools.Remove(backupPath + "/" + fileName)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *Postgresql15Controller) RestoreBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
err = c.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 还原失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (c *Postgresql15Controller) UserList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
type user struct {
|
||||
User string `json:"user"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
raw := tools.Exec(`echo "\du" | su - postgres -c "psql"`)
|
||||
users := strings.Split(raw, "\n")
|
||||
if len(users) < 4 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "用户列表为空")
|
||||
}
|
||||
users = users[3:]
|
||||
|
||||
var userList []user
|
||||
for _, u := range users {
|
||||
userInfo := strings.Split(u, "|")
|
||||
if len(userInfo) != 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
userList = append(userList, user{
|
||||
User: strings.TrimSpace(userInfo[0]),
|
||||
Role: strings.TrimSpace(userInfo[1]),
|
||||
})
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(userList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []user{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(userList) {
|
||||
endIndex = len(userList)
|
||||
}
|
||||
pagedUsers := userList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(userList),
|
||||
"items": pagedUsers,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (c *Postgresql15Controller) AddUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`)
|
||||
tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`)
|
||||
|
||||
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
|
||||
tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (c *Postgresql15Controller) DeleteUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
tools.Exec(`echo "DROP USER ` + user + `;" | su - postgres -c "psql"`)
|
||||
tools.Exec(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (c *Postgresql15Controller) SetUserPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql15")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
tools.Exec(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,612 +0,0 @@
|
||||
package postgresql16
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Postgresql16Controller struct {
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func NewPostgresql16Controller() *Postgresql16Controller {
|
||||
return &Postgresql16Controller{
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *Postgresql16Controller) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *Postgresql16Controller) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *Postgresql16Controller) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *Postgresql16Controller) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *Postgresql16Controller) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop postgresql")
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *Postgresql16Controller) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/postgresql/data/postgresql.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// GetUserConfig 获取用户配置
|
||||
func (c *Postgresql16Controller) GetUserConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/postgresql/data/pg_hba.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *Postgresql16Controller) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/postgresql/data/postgresql.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// SaveUserConfig 保存用户配置
|
||||
func (c *Postgresql16Controller) SaveUserConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/postgresql/data/pg_hba.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入PostgreSQL配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *Postgresql16Controller) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL 已停止运行")
|
||||
}
|
||||
|
||||
data := []Info{
|
||||
{"启动时间", carbon.Parse(tools.Exec(`echo "select pg_postmaster_start_time();" | su - postgres -c "psql" | sed -n 3p | cut -d'.' -f1`)).ToDateTimeString()},
|
||||
{"进程 PID", tools.Exec(`echo "select pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)},
|
||||
{"进程数", tools.Exec(`ps aux | grep postgres | grep -v grep | wc -l`)},
|
||||
{"总连接数", tools.Exec(`echo "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();" | su - postgres -c "psql" | sed -n 3p`)},
|
||||
{"空间占用", tools.Exec(`echo "select pg_size_pretty(pg_database_size('postgres'));" | su - postgres -c "psql" | sed -n 3p`)},
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
|
||||
// Log 获取日志
|
||||
func (c *Postgresql16Controller) Log(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Exec("tail -n 100 /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (c *Postgresql16Controller) ClearLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("echo '' > /www/server/postgresql/logs/postgresql-" + carbon.Now().ToDateString() + ".log")
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DatabaseList 获取数据库列表
|
||||
func (c *Postgresql16Controller) DatabaseList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status postgresql | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "PostgreSQL 已停止运行")
|
||||
}
|
||||
|
||||
raw := tools.Exec(`echo "\l" | su - postgres -c "psql"`)
|
||||
databases := strings.Split(raw, "\n")
|
||||
databases = databases[3 : len(databases)-1]
|
||||
|
||||
type database struct {
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
|
||||
var databaseList []database
|
||||
for _, db := range databases {
|
||||
parts := strings.Split(db, "|")
|
||||
if len(parts) != 9 || len(strings.TrimSpace(parts[0])) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
databaseList = append(databaseList, database{
|
||||
Name: strings.TrimSpace(parts[0]),
|
||||
Owner: strings.TrimSpace(parts[1]),
|
||||
Encoding: strings.TrimSpace(parts[2]),
|
||||
})
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(databaseList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []database{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(databaseList) {
|
||||
endIndex = len(databaseList)
|
||||
}
|
||||
pagedDatabases := databaseList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(databaseList),
|
||||
"items": pagedDatabases,
|
||||
})
|
||||
}
|
||||
|
||||
// AddDatabase 添加数据库
|
||||
func (c *Postgresql16Controller) AddDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
tools.Exec(`echo "CREATE DATABASE ` + database + `;" | su - postgres -c "psql"`)
|
||||
tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`)
|
||||
tools.Exec(`echo "ALTER DATABASE ` + database + ` OWNER TO ` + user + `;" | su - postgres -c "psql"`)
|
||||
tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`)
|
||||
|
||||
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
|
||||
tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// DeleteDatabase 删除数据库
|
||||
func (c *Postgresql16Controller) DeleteDatabase(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:postgres,template0,template1",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec(`echo "DROP DATABASE ` + database + `;" | su - postgres -c "psql"`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BackupList 获取备份列表
|
||||
func (c *Postgresql16Controller) BackupList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
backupList, err := c.backup.PostgresqlList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 获取备份列表失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取备份列表失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *Postgresql16Controller) UploadBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
name := file.GetClientOriginalName()
|
||||
_, err = file.StoreAs(backupPath, name)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *Postgresql16Controller) CreateBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
database := ctx.Request().Input("database")
|
||||
err = c.backup.PostgresqlBackup(database)
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 创建备份失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "创建备份失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *Postgresql16Controller) DeleteBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/postgresql"
|
||||
fileName := ctx.Request().Input("name")
|
||||
tools.Remove(backupPath + "/" + fileName)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *Postgresql16Controller) RestoreBackup(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|min_len:1|max_len:255",
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$|not_in:information_schema,mysql,performance_schema,sys",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
err = c.backup.PostgresqlRestore(ctx.Request().Input("database"), ctx.Request().Input("name"))
|
||||
if err != nil {
|
||||
facades.Log().Error("[PostgreSQL] 还原失败:" + err.Error())
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "还原失败: "+err.Error())
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UserList 用户列表
|
||||
func (c *Postgresql16Controller) UserList(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
type user struct {
|
||||
User string `json:"user"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
raw := tools.Exec(`echo "\du" | su - postgres -c "psql"`)
|
||||
users := strings.Split(raw, "\n")
|
||||
if len(users) < 4 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "用户列表为空")
|
||||
}
|
||||
users = users[3:]
|
||||
|
||||
var userList []user
|
||||
for _, u := range users {
|
||||
userInfo := strings.Split(u, "|")
|
||||
if len(userInfo) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
userList = append(userList, user{
|
||||
User: strings.TrimSpace(userInfo[0]),
|
||||
Role: strings.TrimSpace(userInfo[1]),
|
||||
})
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(userList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []user{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(userList) {
|
||||
endIndex = len(userList)
|
||||
}
|
||||
pagedUsers := userList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(userList),
|
||||
"items": pagedUsers,
|
||||
})
|
||||
}
|
||||
|
||||
// AddUser 添加用户
|
||||
func (c *Postgresql16Controller) AddUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"database": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
database := ctx.Request().Input("database")
|
||||
tools.Exec(`echo "CREATE USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`)
|
||||
tools.Exec(`echo "GRANT ALL PRIVILEGES ON DATABASE ` + database + ` TO ` + user + `;" | su - postgres -c "psql"`)
|
||||
|
||||
userConfig := "host " + database + " " + user + " 127.0.0.1/32 scram-sha-256"
|
||||
tools.Exec(`echo "` + userConfig + `" >> /www/server/postgresql/data/pg_hba.conf`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (c *Postgresql16Controller) DeleteUser(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
tools.Exec(`echo "DROP USER ` + user + `;" | su - postgres -c "psql"`)
|
||||
tools.Exec(`sed -i '/` + user + `/d' /www/server/postgresql/data/pg_hba.conf`)
|
||||
|
||||
return c.Reload(ctx)
|
||||
}
|
||||
|
||||
// SetUserPassword 设置用户密码
|
||||
func (c *Postgresql16Controller) SetUserPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "postgresql16")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"user": "required|min_len:1|max_len:255|regex:^[a-zA-Z][a-zA-Z0-9_]+$",
|
||||
"password": "required|min_len:8|max_len:255",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
tools.Exec(`echo "ALTER USER ` + user + ` WITH PASSWORD '` + password + `';" | su - postgres -c "psql"`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
package pureftpd
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type PureFtpdController struct {
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
func NewPureFtpdController() *PureFtpdController {
|
||||
return &PureFtpdController{}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *PureFtpdController) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *PureFtpdController) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload pure-ftpd")
|
||||
status := tools.Exec("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *PureFtpdController) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart pure-ftpd")
|
||||
status := tools.Exec("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *PureFtpdController) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start pure-ftpd")
|
||||
status := tools.Exec("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *PureFtpdController) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop pure-ftpd")
|
||||
status := tools.Exec("systemctl status pure-ftpd | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (c *PureFtpdController) List(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
listRaw := tools.Exec("pure-pw list")
|
||||
if len(listRaw) == 0 {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []User{},
|
||||
})
|
||||
}
|
||||
|
||||
listArr := strings.Split(listRaw, "\n")
|
||||
var users []User
|
||||
for _, v := range listArr {
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
match := regexp.MustCompile(`(\S+)\s+(\S+)`).FindStringSubmatch(v)
|
||||
users = append(users, User{
|
||||
Username: match[1],
|
||||
Path: strings.Replace(match[2], "/./", "/", 1),
|
||||
})
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(users) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []User{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(users) {
|
||||
endIndex = len(users)
|
||||
}
|
||||
pagedUsers := users[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(users),
|
||||
"items": pagedUsers,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加用户
|
||||
func (c *PureFtpdController) Add(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"username": "required",
|
||||
"password": "required|min_len:6",
|
||||
"path": "required",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
username := ctx.Request().Input("username")
|
||||
password := ctx.Request().Input("password")
|
||||
path := ctx.Request().Input("path")
|
||||
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
if !tools.Exists(path) {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "目录不存在")
|
||||
}
|
||||
|
||||
tools.Chmod(path, 0755)
|
||||
tools.Chown(path, "www", "www")
|
||||
tools.Exec(`yes '` + password + `' | pure-pw useradd ` + username + ` -u www -g www -d ` + path)
|
||||
tools.Exec("pure-pw mkdb")
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (c *PureFtpdController) Delete(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"username": "required",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
username := ctx.Request().Input("username")
|
||||
|
||||
tools.Exec("pure-pw userdel " + username + " -m")
|
||||
tools.Exec("pure-pw mkdb")
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (c *PureFtpdController) ChangePassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"username": "required",
|
||||
"password": "required|min_len:6",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
username := ctx.Request().Input("username")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
tools.Exec(`yes '` + password + `' | pure-pw passwd ` + username + ` -m`)
|
||||
tools.Exec("pure-pw mkdb")
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetPort 获取端口
|
||||
func (c *PureFtpdController) GetPort(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
port := tools.Exec(`cat /www/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`)
|
||||
if len(port) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取PureFtpd端口失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, port)
|
||||
}
|
||||
|
||||
// SetPort 设置端口
|
||||
func (c *PureFtpdController) SetPort(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "pureftpd")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"port": "required",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
port := ctx.Request().Input("port")
|
||||
tools.Exec(`sed -i "s/Bind.*/Bind 0.0.0.0,` + port + `/g" /www/server/pure-ftpd/etc/pure-ftpd.conf`)
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("firewall-cmd --zone=public --add-port=" + port + "/tcp --permanent")
|
||||
tools.Exec("firewall-cmd --reload")
|
||||
} else {
|
||||
tools.Exec("ufw allow " + port + "/tcp")
|
||||
tools.Exec("ufw reload")
|
||||
}
|
||||
tools.Exec("systemctl restart pure-ftpd")
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type RedisController struct {
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func NewRedisController() *RedisController {
|
||||
return &RedisController{}
|
||||
}
|
||||
|
||||
// Status 获取运行状态
|
||||
func (c *RedisController) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载配置
|
||||
func (c *RedisController) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl reload redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启服务
|
||||
func (c *RedisController) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl restart redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (c *RedisController) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl start redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
}
|
||||
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止服务
|
||||
func (c *RedisController) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec("systemctl stop redis")
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if len(status) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis状态失败")
|
||||
}
|
||||
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *RedisController) GetConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
// 获取配置
|
||||
config := tools.Read("/www/server/redis/redis.conf")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis配置失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *RedisController) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if len(config) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "配置不能为空")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/redis/redis.conf", config, 0644) {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "写入Redis配置失败")
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (c *RedisController) Load(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "redis")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec("systemctl status redis | grep Active | grep -v grep | awk '{print $2}'")
|
||||
if status != "active" {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "Redis 已停止运行")
|
||||
}
|
||||
|
||||
raw := tools.Exec("redis-cli info")
|
||||
if len(raw) == 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取Redis负载失败")
|
||||
}
|
||||
|
||||
infoLines := strings.Split(raw, "\n")
|
||||
dataRaw := make(map[string]string)
|
||||
|
||||
for _, item := range infoLines {
|
||||
parts := strings.Split(item, ":")
|
||||
if len(parts) == 2 {
|
||||
dataRaw[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
data := []Info{
|
||||
{"TCP 端口", dataRaw["tcp_port"]},
|
||||
{"已运行天数", dataRaw["uptime_in_days"]},
|
||||
{"连接的客户端数", dataRaw["connected_clients"]},
|
||||
{"已分配的内存总量", dataRaw["used_memory_human"]},
|
||||
{"占用内存总量", dataRaw["used_memory_rss_human"]},
|
||||
{"占用内存峰值", dataRaw["used_memory_peak_human"]},
|
||||
{"内存碎片比率", dataRaw["mem_fragmentation_ratio"]},
|
||||
{"运行以来连接过的客户端的总数", dataRaw["total_connections_received"]},
|
||||
{"运行以来执行过的命令的总数", dataRaw["total_commands_processed"]},
|
||||
{"每秒执行的命令数", dataRaw["instantaneous_ops_per_sec"]},
|
||||
{"查找数据库键成功次数", dataRaw["keyspace_hits"]},
|
||||
{"查找数据库键失败次数", dataRaw["keyspace_misses"]},
|
||||
{"最近一次 fork() 操作耗费的毫秒数", dataRaw["latest_fork_usec"]},
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, data)
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
package s3fs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type S3fsController struct {
|
||||
setting services.Setting
|
||||
}
|
||||
|
||||
type s3fs struct {
|
||||
ID int64 `json:"id"`
|
||||
Path string `json:"path"`
|
||||
Bucket string `json:"bucket"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
func NewS3fsController() *S3fsController {
|
||||
return &S3fsController{
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 所有 S3fs 挂载
|
||||
func (c *S3fsController) List(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "s3fs")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
|
||||
var s3fsList []s3fs
|
||||
err := sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "获取 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(s3fsList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []s3fs{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(s3fsList) {
|
||||
endIndex = len(s3fsList)
|
||||
}
|
||||
pagedS3fsList := s3fsList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(s3fsList),
|
||||
"items": pagedS3fsList,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加 S3fs 挂载
|
||||
func (c *S3fsController) Add(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "s3fs")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"ak": "required|regex:^[a-zA-Z0-9]*$",
|
||||
"sk": "required|regex:^[a-zA-Z0-9]*$",
|
||||
"bucket": "required|regex:^[a-zA-Z0-9_-]*$",
|
||||
"url": "required|full_url",
|
||||
"path": "required|regex:^/[a-zA-Z0-9_-]+$",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, validator.Errors().One())
|
||||
}
|
||||
|
||||
ak := ctx.Request().Input("ak")
|
||||
sk := ctx.Request().Input("sk")
|
||||
path := ctx.Request().Input("path")
|
||||
bucket := ctx.Request().Input("bucket")
|
||||
url := ctx.Request().Input("url")
|
||||
|
||||
// 检查下地域节点中是否包含bucket,如果包含了,肯定是错误的
|
||||
if strings.Contains(url, bucket) {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "地域节点不能包含 Bucket 名称")
|
||||
}
|
||||
|
||||
// 检查挂载目录是否存在且为空
|
||||
if !tools.Exists(path) {
|
||||
tools.Mkdir(path, 0755)
|
||||
}
|
||||
if !tools.Empty(path) {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载目录必须为空")
|
||||
}
|
||||
|
||||
var s3fsList []s3fs
|
||||
err = sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
for _, s := range s3fsList {
|
||||
if s.Path == path {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "路径已存在")
|
||||
}
|
||||
}
|
||||
|
||||
id := carbon.Now().TimestampMilli()
|
||||
password := ak + ":" + sk
|
||||
tools.Write("/etc/passwd-s3fs-"+cast.ToString(id), password, 0600)
|
||||
tools.Exec(`echo 's3fs#` + bucket + ` ` + path + ` fuse _netdev,allow_other,nonempty,url=` + url + `,passwd_file=/etc/passwd-s3fs-` + cast.ToString(id) + ` 0 0' >> /etc/fstab`)
|
||||
mountCheck := tools.Exec("mount -a 2>&1")
|
||||
if len(mountCheck) != 0 {
|
||||
tools.Exec(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck)
|
||||
}
|
||||
dfCheck := tools.Exec("df -h | grep " + path + " 2>&1")
|
||||
if len(dfCheck) == 0 {
|
||||
tools.Exec(`sed -i 's@^s3fs#` + bucket + `\s` + path + `.*$@@g' /etc/fstab`)
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "挂载失败,请检查配置是否正确")
|
||||
}
|
||||
|
||||
s3fsList = append(s3fsList, s3fs{
|
||||
ID: id,
|
||||
Path: path,
|
||||
Bucket: bucket,
|
||||
Url: url,
|
||||
})
|
||||
json, err := sonic.MarshalString(s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
|
||||
}
|
||||
err = c.setting.Set("s3fs", json)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "添加 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Delete 删除 S3fs 挂载
|
||||
func (c *S3fsController) Delete(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "s3fs")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
id := ctx.Request().Input("id")
|
||||
if len(id) == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不能为空")
|
||||
}
|
||||
|
||||
var s3fsList []s3fs
|
||||
err := sonic.UnmarshalString(c.setting.Get("s3fs", "[]"), &s3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "获取 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
var mount s3fs
|
||||
for _, s := range s3fsList {
|
||||
if cast.ToString(s.ID) == id {
|
||||
mount = s
|
||||
break
|
||||
}
|
||||
}
|
||||
if mount.ID == 0 {
|
||||
return controllers.Error(ctx, http.StatusUnprocessableEntity, "挂载ID不存在")
|
||||
}
|
||||
|
||||
tools.Exec(`fusermount -u '` + mount.Path + `' 2>&1`)
|
||||
tools.Exec(`umount '` + mount.Path + `' 2>&1`)
|
||||
tools.Exec(`sed -i 's@^s3fs#` + mount.Bucket + `\s` + mount.Path + `.*$@@g' /etc/fstab`)
|
||||
mountCheck := tools.Exec("mount -a 2>&1")
|
||||
if len(mountCheck) != 0 {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "检测到/etc/fstab有误: "+mountCheck)
|
||||
}
|
||||
tools.Remove("/etc/passwd-s3fs-" + cast.ToString(mount.ID))
|
||||
|
||||
var newS3fsList []s3fs
|
||||
for _, s := range s3fsList {
|
||||
if s.ID != mount.ID {
|
||||
newS3fsList = append(newS3fsList, s)
|
||||
}
|
||||
}
|
||||
json, err := sonic.MarshalString(newS3fsList)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
|
||||
}
|
||||
err = c.setting.Set("s3fs", json)
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusInternalServerError, "删除 S3fs 挂载失败")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,405 +0,0 @@
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"panel/pkg/tools"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
)
|
||||
|
||||
type SupervisorController struct {
|
||||
ServiceName string
|
||||
}
|
||||
|
||||
func NewSupervisorController() *SupervisorController {
|
||||
var serviceName string
|
||||
if tools.IsRHEL() {
|
||||
serviceName = "supervisord"
|
||||
} else {
|
||||
serviceName = "supervisor"
|
||||
}
|
||||
|
||||
return &SupervisorController{
|
||||
ServiceName: serviceName,
|
||||
}
|
||||
}
|
||||
|
||||
// Status 状态
|
||||
func (c *SupervisorController) Status(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Start 启动
|
||||
func (c *SupervisorController) Start(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl start ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止
|
||||
func (c *SupervisorController) Stop(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl stop ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status != "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart 重启
|
||||
func (c *SupervisorController) Restart(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl restart ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reload 重载
|
||||
func (c *SupervisorController) Reload(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec(`systemctl reload ` + c.ServiceName)
|
||||
status := tools.Exec(`systemctl status ` + c.ServiceName + ` | grep Active | grep -v grep | awk '{print $2}'`)
|
||||
if status == "active" {
|
||||
return controllers.Success(ctx, true)
|
||||
} else {
|
||||
return controllers.Success(ctx, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Log 日志
|
||||
func (c *SupervisorController) Log(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
log := tools.Exec(`tail -n 200 /var/log/supervisor/supervisord.log`)
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (c *SupervisorController) ClearLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
tools.Exec(`echo "" > /var/log/supervisor/supervisord.log`)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Config 获取配置
|
||||
func (c *SupervisorController) Config(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
var config string
|
||||
if tools.IsRHEL() {
|
||||
config = tools.Read(`/etc/supervisord.conf`)
|
||||
} else {
|
||||
config = tools.Read(`/etc/supervisor/supervisord.conf`)
|
||||
}
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *SupervisorController) SaveConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
config := ctx.Request().Input("config")
|
||||
if tools.IsRHEL() {
|
||||
tools.Write(`/etc/supervisord.conf`, config, 0644)
|
||||
} else {
|
||||
tools.Write(`/etc/supervisor/supervisord.conf`, config, 0644)
|
||||
}
|
||||
|
||||
return c.Restart(ctx)
|
||||
}
|
||||
|
||||
// Processes 进程列表
|
||||
func (c *SupervisorController) Processes(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
|
||||
type process struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Pid string `json:"pid"`
|
||||
Uptime string `json:"uptime"`
|
||||
}
|
||||
|
||||
out := tools.Exec(`supervisorctl status | awk '{print $1}'`)
|
||||
var processList []process
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var p process
|
||||
p.Name = line
|
||||
p.Status = tools.Exec(`supervisorctl status ` + line + ` | awk '{print $2}'`)
|
||||
if p.Status == "RUNNING" {
|
||||
p.Pid = strings.ReplaceAll(tools.Exec(`supervisorctl status `+line+` | awk '{print $4}'`), ",", "")
|
||||
p.Uptime = tools.Exec(`supervisorctl status ` + line + ` | awk '{print $6}'`)
|
||||
} else {
|
||||
p.Pid = "-"
|
||||
p.Uptime = "-"
|
||||
}
|
||||
processList = append(processList, p)
|
||||
}
|
||||
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(processList) {
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []process{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(processList) {
|
||||
endIndex = len(processList)
|
||||
}
|
||||
pagedProcessList := processList[startIndex:endIndex]
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": len(processList),
|
||||
"items": pagedProcessList,
|
||||
})
|
||||
}
|
||||
|
||||
// StartProcess 启动进程
|
||||
func (c *SupervisorController) StartProcess(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
tools.Exec(`supervisorctl start ` + process)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// StopProcess 停止进程
|
||||
func (c *SupervisorController) StopProcess(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
tools.Exec(`supervisorctl stop ` + process)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// RestartProcess 重启进程
|
||||
func (c *SupervisorController) RestartProcess(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
tools.Exec(`supervisorctl restart ` + process)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ProcessLog 进程日志
|
||||
func (c *SupervisorController) ProcessLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
var logPath string
|
||||
if tools.IsRHEL() {
|
||||
logPath = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
} else {
|
||||
logPath = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
}
|
||||
|
||||
log := tools.Exec(`tail -n 200 ` + logPath)
|
||||
return controllers.Success(ctx, log)
|
||||
}
|
||||
|
||||
// ClearProcessLog 清空进程日志
|
||||
func (c *SupervisorController) ClearProcessLog(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
var logPath string
|
||||
if tools.IsRHEL() {
|
||||
logPath = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
} else {
|
||||
logPath = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
}
|
||||
|
||||
tools.Exec(`echo "" > ` + logPath)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ProcessConfig 获取进程配置
|
||||
func (c *SupervisorController) ProcessConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
process := ctx.Request().Query("process")
|
||||
var config string
|
||||
if tools.IsRHEL() {
|
||||
config = tools.Read(`/etc/supervisord.d/` + process + `.conf`)
|
||||
} else {
|
||||
config = tools.Read(`/etc/supervisor/conf.d/` + process + `.conf`)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveProcessConfig 保存进程配置
|
||||
func (c *SupervisorController) SaveProcessConfig(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
config := ctx.Request().Input("config")
|
||||
if tools.IsRHEL() {
|
||||
tools.Write(`/etc/supervisord.d/`+process+`.conf`, config, 0644)
|
||||
} else {
|
||||
tools.Write(`/etc/supervisor/conf.d/`+process+`.conf`, config, 0644)
|
||||
}
|
||||
tools.Exec(`supervisorctl reread`)
|
||||
tools.Exec(`supervisorctl update`)
|
||||
tools.Exec(`supervisorctl start ` + process)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// AddProcess 添加进程
|
||||
func (c *SupervisorController) AddProcess(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|alpha_dash",
|
||||
"user": "required|alpha_dash",
|
||||
"path": "required",
|
||||
"command": "required",
|
||||
"num": "required",
|
||||
})
|
||||
if err != nil {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
name := ctx.Request().Input("name")
|
||||
user := ctx.Request().Input("user")
|
||||
path := ctx.Request().Input("path")
|
||||
command := ctx.Request().Input("command")
|
||||
num := ctx.Request().InputInt("num", 1)
|
||||
config := `[program:` + name + `]
|
||||
command=` + command + `
|
||||
process_name=%(program_name)s
|
||||
directory=` + path + `
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=` + user + `
|
||||
numprocs=` + strconv.Itoa(num) + `
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/` + name + `.log
|
||||
stdout_logfile_maxbytes=2MB
|
||||
`
|
||||
if tools.IsRHEL() {
|
||||
tools.Write(`/etc/supervisord.d/`+name+`.conf`, config, 0644)
|
||||
} else {
|
||||
tools.Write(`/etc/supervisor/conf.d/`+name+`.conf`, config, 0644)
|
||||
}
|
||||
tools.Exec(`supervisorctl reread`)
|
||||
tools.Exec(`supervisorctl update`)
|
||||
tools.Exec(`supervisorctl start ` + name)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteProcess 删除进程
|
||||
func (c *SupervisorController) DeleteProcess(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "supervisor")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
process := ctx.Request().Input("process")
|
||||
tools.Exec(`supervisorctl stop ` + process)
|
||||
var logPath string
|
||||
if tools.IsRHEL() {
|
||||
logPath = tools.Exec(`cat '/etc/supervisord.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
tools.Remove(`/etc/supervisord.d/` + process + `.conf`)
|
||||
} else {
|
||||
logPath = tools.Exec(`cat '/etc/supervisor/conf.d/` + process + `.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`)
|
||||
tools.Remove(`/etc/supervisor/conf.d/` + process + `.conf`)
|
||||
}
|
||||
tools.Remove(logPath)
|
||||
tools.Exec(`supervisorctl reread`)
|
||||
tools.Exec(`supervisorctl update`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
package toolbox
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/http/controllers"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type ToolBoxController struct {
|
||||
}
|
||||
|
||||
func NewToolBoxController() *ToolBoxController {
|
||||
return &ToolBoxController{}
|
||||
}
|
||||
|
||||
// GetDNS 获取 DNS 信息
|
||||
func (c *ToolBoxController) GetDNS(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
raw := tools.Read("/etc/resolv.conf")
|
||||
match := regexp.MustCompile(`nameserver\s+(\S+)`).FindAllStringSubmatch(raw, -1)
|
||||
if len(match) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "找不到 DNS 信息")
|
||||
}
|
||||
|
||||
var dns []string
|
||||
for _, m := range match {
|
||||
dns = append(dns, m[1])
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, dns)
|
||||
}
|
||||
|
||||
// SetDNS 设置 DNS 信息
|
||||
func (c *ToolBoxController) SetDNS(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
dns1 := ctx.Request().Input("dns1")
|
||||
dns2 := ctx.Request().Input("dns2")
|
||||
|
||||
if len(dns1) == 0 || len(dns2) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "DNS 信息不能为空")
|
||||
}
|
||||
|
||||
var dns string
|
||||
dns += "nameserver " + dns1 + "\n"
|
||||
dns += "nameserver " + dns2 + "\n"
|
||||
|
||||
tools.Write("/etc/resolv.conf", dns, 0644)
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetSWAP 获取 SWAP 信息
|
||||
func (c *ToolBoxController) GetSWAP(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
var total, size, used, free string
|
||||
if tools.Exists("/www/swap") {
|
||||
s, _ := tools.FileSize("/www/swap")
|
||||
size = strconv.Itoa(int(s / 1024 / 1024))
|
||||
total = tools.FormatBytes(float64(s))
|
||||
} else {
|
||||
size = "0.00 B"
|
||||
total = "0.00 B"
|
||||
}
|
||||
|
||||
raw := tools.Exec("LC_ALL=C free | grep Swap")
|
||||
match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) > 0 {
|
||||
used = tools.FormatBytes(cast.ToFloat64(match[2]) * 1024)
|
||||
free = tools.FormatBytes(cast.ToFloat64(match[3]) * 1024)
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"size": size,
|
||||
"used": used,
|
||||
"free": free,
|
||||
})
|
||||
}
|
||||
|
||||
// SetSWAP 设置 SWAP 信息
|
||||
func (c *ToolBoxController) SetSWAP(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
size := ctx.Request().InputInt("size")
|
||||
|
||||
if tools.Exists("/www/swap") {
|
||||
tools.Exec("swapoff /www/swap")
|
||||
tools.Exec("rm -f /www/swap")
|
||||
tools.Exec("sed -i '/www\\/swap/d' /etc/fstab")
|
||||
}
|
||||
|
||||
if size > 1 {
|
||||
free := tools.Exec("df -k /www | awk '{print $4}' | tail -n 1")
|
||||
if cast.ToInt64(free)*1024 < int64(size)*1024*1024 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "磁盘空间不足,当前剩余 "+tools.FormatBytes(cast.ToFloat64(free)))
|
||||
}
|
||||
|
||||
if strings.Contains(tools.Exec("df -T /www | awk '{print $2}' | tail -n 1"), "btrfs") {
|
||||
tools.Exec("btrfs filesystem mkswapfile --size " + cast.ToString(size) + "M --uuid clear /www/swap")
|
||||
} else {
|
||||
tools.Exec("dd if=/dev/zero of=/www/swap bs=1M count=" + cast.ToString(size))
|
||||
tools.Exec("mkswap -f /www/swap")
|
||||
tools.Chmod("/www/swap", 0600)
|
||||
}
|
||||
tools.Exec("swapon /www/swap")
|
||||
tools.Exec("echo '/www/swap swap swap defaults 0 0' >> /etc/fstab")
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetTimezone 获取时区
|
||||
func (c *ToolBoxController) GetTimezone(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
raw := tools.Exec("LC_ALL=C timedatectl | grep zone")
|
||||
match := regexp.MustCompile(`zone:\s+(\S+)`).FindStringSubmatch(raw)
|
||||
if len(match) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "找不到时区信息")
|
||||
}
|
||||
|
||||
zonesRaw := tools.Exec("LC_ALL=C timedatectl list-timezones")
|
||||
zones := strings.Split(zonesRaw, "\n")
|
||||
|
||||
return controllers.Success(ctx, http.Json{
|
||||
"zone": match[1],
|
||||
"zones": zones,
|
||||
})
|
||||
}
|
||||
|
||||
// SetTimezone 设置时区
|
||||
func (c *ToolBoxController) SetTimezone(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
timezone := ctx.Request().Input("timezone")
|
||||
if len(timezone) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "时区不能为空")
|
||||
}
|
||||
|
||||
tools.Exec("timedatectl set-timezone " + timezone)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetHosts 获取 hosts 信息
|
||||
func (c *ToolBoxController) GetHosts(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
return controllers.Success(ctx, tools.Read("/etc/hosts"))
|
||||
}
|
||||
|
||||
// SetHosts 设置 hosts 信息
|
||||
func (c *ToolBoxController) SetHosts(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
hosts := ctx.Request().Input("hosts")
|
||||
if len(hosts) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "hosts 信息不能为空")
|
||||
}
|
||||
|
||||
tools.Write("/etc/hosts", hosts, 0644)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
|
||||
// SetRootPassword 设置 root 密码
|
||||
func (c *ToolBoxController) SetRootPassword(ctx http.Context) http.Response {
|
||||
check := controllers.Check(ctx, "toolbox")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
|
||||
password := ctx.Request().Input("password")
|
||||
if len(password) == 0 {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "密码不能为空")
|
||||
}
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(password) {
|
||||
return controllers.Error(ctx, http.StatusBadRequest, "密码必须为 6-20 位字母、数字或特殊字符")
|
||||
}
|
||||
|
||||
password = strings.ReplaceAll(password, `'`, `\'`)
|
||||
tools.Exec(`yes '` + password + `' | passwd root`)
|
||||
|
||||
return controllers.Success(ctx, nil)
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type SafeController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewSafeController() *SafeController {
|
||||
return &SafeController{
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
|
||||
// GetFirewallStatus 获取防火墙状态
|
||||
func (r *SafeController) GetFirewallStatus(ctx http.Context) http.Response {
|
||||
return Success(ctx, r.firewallStatus())
|
||||
}
|
||||
|
||||
// SetFirewallStatus 设置防火墙状态
|
||||
func (r *SafeController) SetFirewallStatus(ctx http.Context) http.Response {
|
||||
var out string
|
||||
if ctx.Request().InputBool("status") {
|
||||
if tools.IsRHEL() {
|
||||
out = tools.Exec("systemctl start firewalld")
|
||||
} else {
|
||||
out = tools.Exec("echo y | ufw enable")
|
||||
}
|
||||
} else {
|
||||
if tools.IsRHEL() {
|
||||
out = tools.Exec("systemctl stop firewalld")
|
||||
} else {
|
||||
out = tools.Exec("ufw disable")
|
||||
}
|
||||
}
|
||||
|
||||
return Success(ctx, out)
|
||||
}
|
||||
|
||||
// GetFirewallRules 获取防火墙规则
|
||||
func (r *SafeController) GetFirewallRules(ctx http.Context) http.Response {
|
||||
if !r.firewallStatus() {
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
|
||||
if tools.IsRHEL() {
|
||||
out := tools.Exec("firewall-cmd --list-all 2>&1")
|
||||
match := regexp.MustCompile(`ports: (.*)`).FindStringSubmatch(out)
|
||||
if len(match) == 0 {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
}
|
||||
ports := strings.Split(match[1], " ")
|
||||
var rules []map[string]string
|
||||
for _, port := range ports {
|
||||
rule := strings.Split(port, "/")
|
||||
rules = append(rules, map[string]string{
|
||||
"port": rule[0],
|
||||
"protocol": rule[1],
|
||||
})
|
||||
}
|
||||
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(rules) {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(rules) {
|
||||
endIndex = len(rules)
|
||||
}
|
||||
pagedRules := rules[startIndex:endIndex]
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": len(rules),
|
||||
"items": pagedRules,
|
||||
})
|
||||
} else {
|
||||
out := tools.Exec("ufw status | grep -v '(v6)' | grep ALLOW | awk '{print $1}'")
|
||||
if len(out) == 0 {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
}
|
||||
var rules []map[string]string
|
||||
for _, port := range strings.Split(out, "\n") {
|
||||
rule := strings.Split(port, "/")
|
||||
rules = append(rules, map[string]string{
|
||||
"port": rule[0],
|
||||
"protocol": rule[1],
|
||||
})
|
||||
}
|
||||
|
||||
startIndex := (page - 1) * limit
|
||||
endIndex := page * limit
|
||||
if startIndex > len(rules) {
|
||||
return Success(ctx, http.Json{
|
||||
"total": 0,
|
||||
"items": []map[string]string{},
|
||||
})
|
||||
}
|
||||
if endIndex > len(rules) {
|
||||
endIndex = len(rules)
|
||||
}
|
||||
pagedRules := rules[startIndex:endIndex]
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": len(rules),
|
||||
"items": pagedRules,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// AddFirewallRule 添加防火墙规则
|
||||
func (r *SafeController) AddFirewallRule(ctx http.Context) http.Response {
|
||||
if !r.firewallStatus() {
|
||||
return Error(ctx, http.StatusBadRequest, "防火墙未启动")
|
||||
}
|
||||
|
||||
port := ctx.Request().Input("port")
|
||||
protocol := ctx.Request().Input("protocol")
|
||||
if port == "" || protocol == "" || (protocol != "tcp" && protocol != "udp") {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
// 端口有 2 种写法,一种是 80-443,一种是 80
|
||||
if strings.Contains(port, "-") {
|
||||
ports := strings.Split(port, "-")
|
||||
startPort := cast.ToInt(ports[0])
|
||||
endPort := cast.ToInt(ports[1])
|
||||
if startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535 || startPort > endPort {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
} else {
|
||||
port := cast.ToInt(port)
|
||||
if port < 1 || port > 65535 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
}
|
||||
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
|
||||
tools.Exec("firewall-cmd --add-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
|
||||
tools.Exec("firewall-cmd --reload")
|
||||
} else {
|
||||
// ufw 需要替换 - 为 : 添加
|
||||
if strings.Contains(port, "-") {
|
||||
port = strings.ReplaceAll(port, "-", ":")
|
||||
}
|
||||
tools.Exec("ufw delete allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.Exec("ufw allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.Exec("ufw reload")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteFirewallRule 删除防火墙规则
|
||||
func (r *SafeController) DeleteFirewallRule(ctx http.Context) http.Response {
|
||||
if !r.firewallStatus() {
|
||||
return Error(ctx, http.StatusBadRequest, "防火墙未启动")
|
||||
}
|
||||
|
||||
port := ctx.Request().InputInt("port", 0)
|
||||
protocol := ctx.Request().Input("protocol", "")
|
||||
if port == 0 || protocol == "" {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("firewall-cmd --remove-port=" + cast.ToString(port) + "/" + protocol + " --permanent 2>&1")
|
||||
tools.Exec("firewall-cmd --reload")
|
||||
} else {
|
||||
tools.Exec("ufw delete allow " + cast.ToString(port) + "/" + protocol)
|
||||
tools.Exec("ufw reload")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// firewallStatus 获取防火墙状态
|
||||
func (r *SafeController) firewallStatus() bool {
|
||||
var out string
|
||||
var running bool
|
||||
if tools.IsRHEL() {
|
||||
out = tools.Exec("systemctl status firewalld | grep Active | awk '{print $3}'")
|
||||
if out == "(running)" {
|
||||
running = true
|
||||
} else {
|
||||
running = false
|
||||
}
|
||||
} else {
|
||||
out = tools.Exec("ufw status | grep Status | awk '{print $2}'")
|
||||
if out == "active" {
|
||||
running = true
|
||||
} else {
|
||||
running = false
|
||||
}
|
||||
}
|
||||
|
||||
return running
|
||||
}
|
||||
|
||||
// GetSshStatus 获取 SSH 状态
|
||||
func (r *SafeController) GetSshStatus(ctx http.Context) http.Response {
|
||||
var out string
|
||||
if tools.IsRHEL() {
|
||||
out = tools.Exec("systemctl status sshd | grep Active | awk '{print $3}'")
|
||||
} else {
|
||||
out = tools.Exec("systemctl status ssh | grep Active | awk '{print $3}'")
|
||||
}
|
||||
|
||||
running := false
|
||||
if out == "(running)" {
|
||||
running = true
|
||||
}
|
||||
|
||||
return Success(ctx, running)
|
||||
}
|
||||
|
||||
// SetSshStatus 设置 SSH 状态
|
||||
func (r *SafeController) SetSshStatus(ctx http.Context) http.Response {
|
||||
if ctx.Request().InputBool("status") {
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("systemctl enable sshd")
|
||||
tools.Exec("systemctl start sshd")
|
||||
} else {
|
||||
tools.Exec("systemctl enable ssh")
|
||||
tools.Exec("systemctl start ssh")
|
||||
}
|
||||
} else {
|
||||
if tools.IsRHEL() {
|
||||
tools.Exec("systemctl stop sshd")
|
||||
tools.Exec("systemctl disable sshd")
|
||||
} else {
|
||||
tools.Exec("systemctl stop ssh")
|
||||
tools.Exec("systemctl disable ssh")
|
||||
}
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetSshPort 获取 SSH 端口
|
||||
func (r *SafeController) GetSshPort(ctx http.Context) http.Response {
|
||||
out := tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
return Success(ctx, out)
|
||||
}
|
||||
|
||||
// SetSshPort 设置 SSH 端口
|
||||
func (r *SafeController) SetSshPort(ctx http.Context) http.Response {
|
||||
port := ctx.Request().InputInt("port", 0)
|
||||
if port == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
oldPort := tools.Exec("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
tools.Exec("sed -i 's/#Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
|
||||
tools.Exec("sed -i 's/Port " + oldPort + "/Port " + cast.ToString(port) + "/g' /etc/ssh/sshd_config")
|
||||
|
||||
if status := tools.Exec("systemctl status sshd | grep Active | awk '{print $3}'"); status == "(running)" {
|
||||
tools.Exec("systemctl restart sshd")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetPingStatus 获取 Ping 状态
|
||||
func (r *SafeController) GetPingStatus(ctx http.Context) http.Response {
|
||||
if tools.IsRHEL() {
|
||||
out := tools.Exec(`firewall-cmd --list-all 2>&1`)
|
||||
if !strings.Contains(out, `rule protocol value="icmp" drop`) {
|
||||
return Success(ctx, true)
|
||||
} else {
|
||||
return Success(ctx, false)
|
||||
}
|
||||
} else {
|
||||
config := tools.Read("/etc/ufw/before.rules")
|
||||
if strings.Contains(config, "-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT") {
|
||||
return Success(ctx, true)
|
||||
} else {
|
||||
return Success(ctx, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetPingStatus 设置 Ping 状态
|
||||
func (r *SafeController) SetPingStatus(ctx http.Context) http.Response {
|
||||
if tools.IsRHEL() {
|
||||
if ctx.Request().InputBool("status") {
|
||||
tools.Exec(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
|
||||
} else {
|
||||
tools.Exec(`firewall-cmd --permanent --add-rich-rule='rule protocol value=icmp drop'`)
|
||||
}
|
||||
tools.Exec(`firewall-cmd --reload`)
|
||||
} else {
|
||||
if ctx.Request().InputBool("status") {
|
||||
tools.Exec(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/g' /etc/ufw/before.rules`)
|
||||
} else {
|
||||
tools.Exec(`sed -i 's/-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT/-A ufw-before-input -p icmp --icmp-type echo-request -j DROP/g' /etc/ufw/before.rules`)
|
||||
}
|
||||
tools.Exec(`ufw reload`)
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"panel/pkg/tools"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
type SettingController struct {
|
||||
setting services.Setting
|
||||
}
|
||||
|
||||
func NewSettingController() *SettingController {
|
||||
return &SettingController{
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取设置列表
|
||||
func (r *SettingController) List(ctx http.Context) http.Response {
|
||||
var settings []models.Setting
|
||||
err := facades.Orm().Query().Get(&settings)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 查询设置列表失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
type data struct {
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
Port string `json:"port"`
|
||||
Entrance string `json:"entrance"`
|
||||
WebsitePath string `json:"website_path"`
|
||||
BackupPath string `json:"backup_path"`
|
||||
}
|
||||
|
||||
var result data
|
||||
result.Name = r.setting.Get(models.SettingKeyName)
|
||||
result.Entrance = r.setting.Get(models.SettingKeyEntrance)
|
||||
result.WebsitePath = r.setting.Get(models.SettingKeyWebsitePath)
|
||||
result.BackupPath = r.setting.Get(models.SettingKeyBackupPath)
|
||||
|
||||
var user models.User
|
||||
err = facades.Auth().User(ctx, &user)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 获取用户失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
result.Username = user.Username
|
||||
result.Email = user.Email
|
||||
|
||||
result.Port = tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
|
||||
return Success(ctx, result)
|
||||
}
|
||||
|
||||
// Save 保存设置
|
||||
func (r *SettingController) Save(ctx http.Context) http.Response {
|
||||
name := ctx.Request().Input("name")
|
||||
port := ctx.Request().Input("port")
|
||||
backupPath := ctx.Request().Input("backup_path")
|
||||
websitePath := ctx.Request().Input("website_path")
|
||||
entrance := ctx.Request().Input("entrance")
|
||||
username := ctx.Request().Input("username")
|
||||
email := ctx.Request().Input("email")
|
||||
password := ctx.Request().Input("password")
|
||||
|
||||
if !regexp.MustCompile(`^/[^/]*[^/]$`).MatchString(entrance) || entrance == "/api" {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, "入口格式错误")
|
||||
}
|
||||
|
||||
err := r.setting.Set(models.SettingKeyName, name)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
oldPort := tools.Exec(`cat /www/panel/panel.conf | grep APP_PORT | awk -F '=' '{print $2}' | tr -d '\n'`)
|
||||
if oldPort != port {
|
||||
tools.Exec("sed -i 's/APP_PORT=" + oldPort + "/APP_PORT=" + port + "/g' /www/panel/panel.conf")
|
||||
}
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeyBackupPath, backupPath)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
if !tools.Exists(websitePath) {
|
||||
tools.Mkdir(websitePath, 0755)
|
||||
tools.Chown(websitePath, "www", "www")
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeyWebsitePath, websitePath)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeyEntrance, entrance)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err = facades.Auth().User(ctx, &user)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 获取用户失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if len(username) > 0 {
|
||||
user.Username = username
|
||||
}
|
||||
if len(email) > 0 {
|
||||
user.Email = email
|
||||
}
|
||||
if len(password) > 0 {
|
||||
hash, err := facades.Hash().Make(password)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
user.Password = hash
|
||||
}
|
||||
|
||||
if err = facades.Orm().Query().Save(&user); err != nil {
|
||||
facades.Log().Error("[面板][SettingController] 保存设置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
nethttp "net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/ssh"
|
||||
)
|
||||
|
||||
type SshController struct {
|
||||
AuthMethod ssh.AuthMethod
|
||||
setting services.Setting
|
||||
}
|
||||
|
||||
func NewSshController() *SshController {
|
||||
return &SshController{
|
||||
AuthMethod: ssh.PASSWORD,
|
||||
setting: services.NewSettingImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetInfo 获取 SSH 配置
|
||||
func (r *SshController) GetInfo(ctx http.Context) http.Response {
|
||||
host := r.setting.Get(models.SettingKeySshHost)
|
||||
port := r.setting.Get(models.SettingKeySshPort)
|
||||
user := r.setting.Get(models.SettingKeySshUser)
|
||||
password := r.setting.Get(models.SettingKeySshPassword)
|
||||
if len(host) == 0 || len(user) == 0 || len(password) == 0 {
|
||||
return Error(ctx, http.StatusInternalServerError, "SSH 配置不完整")
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"host": host,
|
||||
"port": cast.ToInt(port),
|
||||
"user": user,
|
||||
"password": password,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateInfo 更新 SSH 配置
|
||||
func (r *SshController) UpdateInfo(ctx http.Context) http.Response {
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"host": "required",
|
||||
"port": "required",
|
||||
"user": "required",
|
||||
"password": "required",
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
host := ctx.Request().Input("host")
|
||||
port := ctx.Request().Input("port")
|
||||
user := ctx.Request().Input("user")
|
||||
password := ctx.Request().Input("password")
|
||||
err = r.setting.Set(models.SettingKeySshHost, host)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeySshPort, port)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeySshUser, user)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
err = r.setting.Set(models.SettingKeySshPassword, password)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 更新配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Session SSH 会话
|
||||
func (r *SshController) Session(ctx http.Context) http.Response {
|
||||
upGrader := websocket.Upgrader{
|
||||
ReadBufferSize: 4096,
|
||||
WriteBufferSize: 4096,
|
||||
CheckOrigin: func(r *nethttp.Request) bool {
|
||||
return true
|
||||
},
|
||||
Subprotocols: []string{ctx.Request().Header("Sec-WebSocket-Protocol")},
|
||||
}
|
||||
|
||||
ws, err := upGrader.Upgrade(ctx.Response().Writer(), ctx.Request().Origin(), nil)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 建立连接失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
config := ssh.SSHClientConfigPassword(
|
||||
r.setting.Get(models.SettingKeySshHost)+":"+r.setting.Get(models.SettingKeySshPort),
|
||||
r.setting.Get(models.SettingKeySshUser),
|
||||
r.setting.Get(models.SettingKeySshPassword),
|
||||
)
|
||||
client, err := ssh.NewSSHClient(config)
|
||||
if err != nil {
|
||||
_ = ws.WriteControl(websocket.CloseMessage,
|
||||
[]byte(err.Error()), time.Now().Add(time.Second))
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
turn, err := ssh.NewTurn(ws, client)
|
||||
if err != nil {
|
||||
_ = ws.WriteControl(websocket.CloseMessage,
|
||||
[]byte(err.Error()), time.Now().Add(time.Second))
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
defer turn.Close()
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
var logBuff = bufPool.Get().(*bytes.Buffer)
|
||||
logBuff.Reset()
|
||||
defer bufPool.Put(logBuff)
|
||||
|
||||
ctx2, cancel := context.WithCancel(context.Background())
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := turn.LoopRead(logBuff, ctx2)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 读取数据失败 ", err.Error())
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := turn.SessionWait()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][SSH] 会话失败 ", err.Error())
|
||||
}
|
||||
cancel()
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
return Success(ctx, logBuff.String())
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
"panel/app/models"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type TaskController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewTaskController() *TaskController {
|
||||
return &TaskController{
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
|
||||
// Status 获取当前任务状态
|
||||
func (r *TaskController) Status(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("status", models.TaskStatusWaiting).OrWhere("status", models.TaskStatusRunning).FirstOrFail(&task)
|
||||
if err == nil {
|
||||
return Success(ctx, http.Json{
|
||||
"task": true,
|
||||
})
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"task": false,
|
||||
})
|
||||
}
|
||||
|
||||
// List 获取任务列表
|
||||
func (r *TaskController) List(ctx http.Context) http.Response {
|
||||
var tasks []models.Task
|
||||
var total int64
|
||||
err := facades.Orm().Query().Paginate(ctx.Request().QueryInt("page", 1), ctx.Request().QueryInt("limit", 10), &tasks, &total)
|
||||
if err != nil {
|
||||
facades.Log().With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("[面板][TaskController] 查询任务列表失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": tasks,
|
||||
})
|
||||
}
|
||||
|
||||
// Log 获取任务日志
|
||||
func (r *TaskController) Log(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
err := facades.Orm().Query().Where("id", ctx.Request().QueryInt("id")).FirstOrFail(&task)
|
||||
if err != nil {
|
||||
facades.Log().With(map[string]any{
|
||||
"id": ctx.Request().QueryInt("id"),
|
||||
"error": err.Error(),
|
||||
}).Error("[面板][TaskController] 查询任务失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
log := tools.Read(task.Log)
|
||||
|
||||
return Success(ctx, log)
|
||||
}
|
||||
|
||||
// Delete 删除任务
|
||||
func (r *TaskController) Delete(ctx http.Context) http.Response {
|
||||
var task models.Task
|
||||
_, err := facades.Orm().Query().Where("id", ctx.Request().Input("id")).Delete(&task)
|
||||
if err != nil {
|
||||
facades.Log().With(map[string]any{
|
||||
"id": ctx.Request().QueryInt("id"),
|
||||
"error": err.Error(),
|
||||
}).Error("[面板][TaskController] 删除任务失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/http/requests"
|
||||
"panel/app/models"
|
||||
)
|
||||
|
||||
type UserController struct {
|
||||
// Dependent services
|
||||
}
|
||||
|
||||
func NewUserController() *UserController {
|
||||
return &UserController{
|
||||
// Inject services
|
||||
}
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (r *UserController) Login(ctx http.Context) http.Response {
|
||||
var loginRequest requests.LoginRequest
|
||||
errors, err := ctx.Request().ValidateRequest(&loginRequest)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, err.Error())
|
||||
}
|
||||
if errors != nil {
|
||||
return Error(ctx, http.StatusUnprocessableEntity, errors.One())
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err = facades.Orm().Query().Where("username", loginRequest.Username).First(&user)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][UserController] 查询用户失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if user.ID == 0 || !facades.Hash().Check(loginRequest.Password, user.Password) {
|
||||
return Error(ctx, http.StatusForbidden, "用户名或密码错误")
|
||||
}
|
||||
|
||||
if facades.Hash().NeedsRehash(user.Password) {
|
||||
user.Password, err = facades.Hash().Make(loginRequest.Password)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][UserController] 更新密码失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
}
|
||||
|
||||
token, loginErr := facades.Auth().LoginUsingID(ctx, user.ID)
|
||||
if loginErr != nil {
|
||||
facades.Log().Error("[面板][UserController] 登录失败 ", loginErr)
|
||||
return Error(ctx, http.StatusInternalServerError, loginErr.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"access_token": token,
|
||||
})
|
||||
}
|
||||
|
||||
// Info 用户信息
|
||||
func (r *UserController) Info(ctx http.Context) http.Response {
|
||||
var user models.User
|
||||
err := facades.Auth().User(ctx, &user)
|
||||
if err != nil {
|
||||
facades.Log().With(map[string]any{
|
||||
"error": err.Error(),
|
||||
}).Error("[面板][UserController] 查询用户信息失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"id": user.ID,
|
||||
"role": []string{"admin"},
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
})
|
||||
}
|
||||
@@ -1,682 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
"panel/app/services"
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type WebsiteController struct {
|
||||
website services.Website
|
||||
setting services.Setting
|
||||
backup services.Backup
|
||||
}
|
||||
|
||||
func NewWebsiteController() *WebsiteController {
|
||||
return &WebsiteController{
|
||||
website: services.NewWebsiteImpl(),
|
||||
setting: services.NewSettingImpl(),
|
||||
backup: services.NewBackupImpl(),
|
||||
}
|
||||
}
|
||||
|
||||
// List 网站列表
|
||||
func (c *WebsiteController) List(ctx http.Context) http.Response {
|
||||
limit := ctx.Request().QueryInt("limit", 10)
|
||||
page := ctx.Request().QueryInt("page", 1)
|
||||
|
||||
total, websites, err := c.website.List(page, limit)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站列表失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"total": total,
|
||||
"items": websites,
|
||||
})
|
||||
}
|
||||
|
||||
// Add 添加网站
|
||||
func (c *WebsiteController) Add(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"name": "required|regex:^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*$|not_exists:websites,name",
|
||||
"domains": "required|slice",
|
||||
"ports": "required|slice",
|
||||
"php": "required",
|
||||
"db": "bool",
|
||||
"db_type": "required_if:db,true",
|
||||
"db_name": "required_if:db,true",
|
||||
"db_user": "required_if:db,true",
|
||||
"db_password": "required_if:db,true",
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
var website services.PanelWebsite
|
||||
website.Name = ctx.Request().Input("name")
|
||||
website.Domains = ctx.Request().InputArray("domains")
|
||||
website.Ports = ctx.Request().InputArray("ports")
|
||||
website.Php = ctx.Request().InputInt("php")
|
||||
website.Db = ctx.Request().InputBool("db")
|
||||
website.DbType = ctx.Request().Input("db_type")
|
||||
website.DbName = ctx.Request().Input("db_name")
|
||||
website.DbUser = ctx.Request().Input("db_user")
|
||||
website.DbPassword = ctx.Request().Input("db_password")
|
||||
|
||||
newSite, err := c.website.Add(website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 添加网站失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, newSite)
|
||||
}
|
||||
|
||||
// Delete 删除网站
|
||||
func (c *WebsiteController) Delete(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
err := c.website.Delete(id)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 删除网站失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "删除网站失败: "+err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetDefaultConfig 获取默认配置
|
||||
func (c *WebsiteController) GetDefaultConfig(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
index := tools.Read("/www/server/openresty/html/index.html")
|
||||
stop := tools.Read("/www/server/openresty/html/stop.html")
|
||||
|
||||
return Success(ctx, http.Json{
|
||||
"index": index,
|
||||
"stop": stop,
|
||||
})
|
||||
}
|
||||
|
||||
// SaveDefaultConfig 保存默认配置
|
||||
func (c *WebsiteController) SaveDefaultConfig(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
index := ctx.Request().Input("index")
|
||||
stop := ctx.Request().Input("stop")
|
||||
|
||||
if !tools.Write("/www/server/openresty/html/index.html", index, 0644) {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存默认配置失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
if !tools.Write("/www/server/openresty/html/stop.html", stop, 0644) {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存默认配置失败")
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (c *WebsiteController) GetConfig(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
config, err := c.website.GetConfig(id)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, config)
|
||||
}
|
||||
|
||||
// SaveConfig 保存配置
|
||||
func (c *WebsiteController) SaveConfig(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
validator, err := ctx.Request().Validate(map[string]string{
|
||||
"domains": "required|slice",
|
||||
"ports": "required|slice",
|
||||
"hsts": "bool",
|
||||
"ssl": "bool",
|
||||
"http_redirect": "bool",
|
||||
"open_basedir": "bool",
|
||||
"waf": "bool",
|
||||
"waf_cache": "required",
|
||||
"waf_mode": "required",
|
||||
"waf_cc_deny": "required",
|
||||
"index": "required",
|
||||
"path": "required",
|
||||
"root": "required",
|
||||
"raw": "required",
|
||||
"php": "required",
|
||||
"ssl_certificate": "required_if:ssl,true",
|
||||
"ssl_certificate_key": "required_if:ssl,true",
|
||||
})
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
if validator.Fails() {
|
||||
return Error(ctx, http.StatusBadRequest, validator.Errors().One())
|
||||
}
|
||||
|
||||
var website models.Website
|
||||
if facades.Orm().Query().Where("id", ctx.Request().Input("id")).FirstOrFail(&website) != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "网站不存在")
|
||||
}
|
||||
|
||||
if !website.Status {
|
||||
return Error(ctx, http.StatusBadRequest, "网站已停用,请先启用")
|
||||
}
|
||||
|
||||
// 原文
|
||||
raw := tools.Read("/www/server/vhost/" + website.Name + ".conf")
|
||||
if strings.TrimSpace(raw) != strings.TrimSpace(ctx.Request().Input("raw")) {
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", ctx.Request().Input("raw"), 0644)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// 目录
|
||||
path := ctx.Request().Input("path")
|
||||
if !tools.Exists(path) {
|
||||
return Error(ctx, http.StatusBadRequest, "网站目录不存在")
|
||||
}
|
||||
website.Path = path
|
||||
|
||||
// 域名
|
||||
domain := "server_name"
|
||||
domains := ctx.Request().InputArray("domains")
|
||||
if len(domains) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "域名不能为空")
|
||||
}
|
||||
for _, v := range domains {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
domain += " " + v
|
||||
}
|
||||
domain += ";"
|
||||
domainConfigOld := tools.Cut(raw, "# server_name标记位开始", "# server_name标记位结束")
|
||||
if len(strings.TrimSpace(domainConfigOld)) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中缺少server_name标记位")
|
||||
}
|
||||
raw = strings.Replace(raw, domainConfigOld, "\n "+domain+"\n ", -1)
|
||||
|
||||
// 端口
|
||||
var port strings.Builder
|
||||
ports := ctx.Request().InputArray("ports")
|
||||
if len(ports) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "端口不能为空")
|
||||
}
|
||||
for i, v := range ports {
|
||||
if _, err := strconv.Atoi(v); err != nil && v != "443 ssl http2" {
|
||||
return Error(ctx, http.StatusBadRequest, "端口格式错误")
|
||||
}
|
||||
if v == "443" && ctx.Request().InputBool("ssl") {
|
||||
v = "443 ssl http2"
|
||||
}
|
||||
if i != len(ports)-1 {
|
||||
port.WriteString(" listen " + v + ";\n")
|
||||
} else {
|
||||
port.WriteString(" listen " + v + ";")
|
||||
}
|
||||
}
|
||||
portConfigOld := tools.Cut(raw, "# port标记位开始", "# port标记位结束")
|
||||
if len(strings.TrimSpace(portConfigOld)) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中缺少port标记位")
|
||||
}
|
||||
raw = strings.Replace(raw, portConfigOld, "\n"+port.String()+"\n ", -1)
|
||||
|
||||
// 运行目录
|
||||
root := tools.Cut(raw, "# root标记位开始", "# root标记位结束")
|
||||
if len(strings.TrimSpace(root)) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中缺少root标记位")
|
||||
}
|
||||
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(root)
|
||||
if len(match) != 2 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中root标记位格式错误")
|
||||
}
|
||||
rootNew := strings.Replace(root, match[1], ctx.Request().Input("root"), -1)
|
||||
raw = strings.Replace(raw, root, rootNew, -1)
|
||||
|
||||
// 默认文件
|
||||
index := tools.Cut(raw, "# index标记位开始", "# index标记位结束")
|
||||
if len(strings.TrimSpace(index)) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中缺少index标记位")
|
||||
}
|
||||
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(index)
|
||||
if len(match) != 2 {
|
||||
return Error(ctx, http.StatusBadRequest, "配置文件中index标记位格式错误")
|
||||
}
|
||||
indexNew := strings.Replace(index, match[1], ctx.Request().Input("index"), -1)
|
||||
raw = strings.Replace(raw, index, indexNew, -1)
|
||||
|
||||
// 防跨站
|
||||
root = ctx.Request().Input("root")
|
||||
if !strings.HasSuffix(root, "/") {
|
||||
root += "/"
|
||||
}
|
||||
if ctx.Request().InputBool("open_basedir") {
|
||||
tools.Write(root+".user.ini", "open_basedir="+path+":/tmp/", 0644)
|
||||
} else {
|
||||
if tools.Exists(root + ".user.ini") {
|
||||
tools.Remove(root + ".user.ini")
|
||||
}
|
||||
}
|
||||
|
||||
// WAF
|
||||
waf := ctx.Request().InputBool("waf")
|
||||
wafStr := "off"
|
||||
if waf {
|
||||
wafStr = "on"
|
||||
}
|
||||
wafMode := ctx.Request().Input("waf_mode", "DYNAMIC")
|
||||
wafCcDeny := ctx.Request().Input("waf_cc_deny", "rate=1000r/m duration=60m")
|
||||
wafCache := ctx.Request().Input("waf_cache", "capacity=50")
|
||||
wafConfig := `# waf标记位开始
|
||||
waf ` + wafStr + `;
|
||||
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
|
||||
waf_mode ` + wafMode + `;
|
||||
waf_cc_deny ` + wafCcDeny + `;
|
||||
waf_cache ` + wafCache + `;
|
||||
`
|
||||
wafConfigOld := tools.Cut(raw, "# waf标记位开始", "# waf标记位结束")
|
||||
if len(strings.TrimSpace(wafConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, wafConfigOld, "", -1)
|
||||
}
|
||||
raw = strings.Replace(raw, "# waf标记位开始", wafConfig, -1)
|
||||
|
||||
// SSL
|
||||
ssl := ctx.Request().InputBool("ssl")
|
||||
website.Ssl = ssl
|
||||
tools.Write("/www/server/vhost/ssl/"+website.Name+".pem", ctx.Request().Input("ssl_certificate"), 0644)
|
||||
tools.Write("/www/server/vhost/ssl/"+website.Name+".key", ctx.Request().Input("ssl_certificate_key"), 0644)
|
||||
if ssl {
|
||||
sslConfig := `# ssl标记位开始
|
||||
ssl_certificate /www/server/vhost/ssl/` + website.Name + `.pem;
|
||||
ssl_certificate_key /www/server/vhost/ssl/` + website.Name + `.key;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_tickets off;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
`
|
||||
if ctx.Request().InputBool("http_redirect") {
|
||||
sslConfig += `# http重定向标记位开始
|
||||
if ($server_port !~ 443){
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
error_page 497 https://$host$request_uri;
|
||||
# http重定向标记位结束
|
||||
`
|
||||
}
|
||||
if ctx.Request().InputBool("hsts") {
|
||||
sslConfig += `# hsts标记位开始
|
||||
add_header Strict-Transport-Security "max-age=63072000" always;
|
||||
# hsts标记位结束
|
||||
`
|
||||
}
|
||||
sslConfigOld := tools.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
|
||||
if len(strings.TrimSpace(sslConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, sslConfigOld, "", -1)
|
||||
}
|
||||
raw = strings.Replace(raw, "# ssl标记位开始", sslConfig, -1)
|
||||
} else {
|
||||
sslConfigOld := tools.Cut(raw, "# ssl标记位开始", "# ssl标记位结束")
|
||||
if len(strings.TrimSpace(sslConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, sslConfigOld, "\n ", -1)
|
||||
}
|
||||
}
|
||||
|
||||
if website.Php != ctx.Request().InputInt("php") {
|
||||
website.Php = ctx.Request().InputInt("php")
|
||||
phpConfigOld := tools.Cut(raw, "# php标记位开始", "# php标记位结束")
|
||||
phpConfig := `
|
||||
include enable-php-` + strconv.Itoa(website.Php) + `.conf;
|
||||
`
|
||||
if len(strings.TrimSpace(phpConfigOld)) != 0 {
|
||||
raw = strings.Replace(raw, phpConfigOld, phpConfig, -1)
|
||||
}
|
||||
}
|
||||
|
||||
err = facades.Orm().Query().Save(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存网站配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644)
|
||||
tools.Write("/www/server/vhost/rewrite/"+website.Name+".conf", ctx.Request().Input("rewrite"), 0644)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (c *WebsiteController) ClearLog(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
err := facades.Orm().Query().Where("id", id).Get(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
tools.Remove("/www/wwwlogs/" + website.Name + ".log")
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UpdateRemark 更新备注
|
||||
func (c *WebsiteController) UpdateRemark(ctx http.Context) http.Response {
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
err := facades.Orm().Query().Where("id", id).Get(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
website.Remark = ctx.Request().Input("remark")
|
||||
err = facades.Orm().Query().Save(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存网站备注失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// BackupList 备份列表
|
||||
func (c *WebsiteController) BackupList(ctx http.Context) http.Response {
|
||||
backupList, err := c.backup.WebsiteList()
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站备份列表失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
return Success(ctx, backupList)
|
||||
}
|
||||
|
||||
// CreateBackup 创建备份
|
||||
func (c *WebsiteController) CreateBackup(ctx http.Context) http.Response {
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
err := facades.Orm().Query().Where("id", id).Get(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "获取网站信息失败: "+err.Error())
|
||||
}
|
||||
|
||||
err = c.backup.WebSiteBackup(website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 备份网站失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "备份网站失败: "+err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// UploadBackup 上传备份
|
||||
func (c *WebsiteController) UploadBackup(ctx http.Context) http.Response {
|
||||
file, err := ctx.Request().File("file")
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/website"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
name := file.GetClientOriginalName()
|
||||
_, err = file.StoreAs(backupPath, name)
|
||||
if err != nil {
|
||||
return Error(ctx, http.StatusBadRequest, "上传文件失败")
|
||||
}
|
||||
|
||||
return Success(ctx, "上传文件成功")
|
||||
}
|
||||
|
||||
// RestoreBackup 还原备份
|
||||
func (c *WebsiteController) RestoreBackup(ctx http.Context) http.Response {
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
fileName := ctx.Request().Input("name")
|
||||
if len(fileName) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
err := facades.Orm().Query().Where("id", id).Get(&website)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "获取网站信息失败: "+err.Error())
|
||||
}
|
||||
|
||||
err = c.backup.WebsiteRestore(website, fileName)
|
||||
if err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 还原网站失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "还原网站失败: "+err.Error())
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// DeleteBackup 删除备份
|
||||
func (c *WebsiteController) DeleteBackup(ctx http.Context) http.Response {
|
||||
fileName := ctx.Request().Input("name")
|
||||
if len(fileName) == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
backupPath := c.setting.Get(models.SettingKeyBackupPath) + "/website"
|
||||
if !tools.Exists(backupPath) {
|
||||
tools.Mkdir(backupPath, 0644)
|
||||
}
|
||||
|
||||
if !tools.Remove(backupPath + "/" + fileName) {
|
||||
return Error(ctx, http.StatusInternalServerError, "删除备份失败")
|
||||
}
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// ResetConfig 重置配置
|
||||
func (c *WebsiteController) ResetConfig(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
if err := facades.Orm().Query().Where("id", id).Get(&website); err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
website.Status = true
|
||||
website.Ssl = false
|
||||
if err := facades.Orm().Query().Save(&website); err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存网站配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
raw := fmt.Sprintf(`
|
||||
# 配置文件中的标记位请勿随意修改,改错将导致面板无法识别!
|
||||
# 有自定义配置需求的,请将自定义的配置写在各标记位下方。
|
||||
server
|
||||
{
|
||||
# port标记位开始
|
||||
listen 80;
|
||||
# port标记位结束
|
||||
# server_name标记位开始
|
||||
server_name localhost;
|
||||
# server_name标记位结束
|
||||
# index标记位开始
|
||||
index index.php index.html;
|
||||
# index标记位结束
|
||||
# root标记位开始
|
||||
root %s;
|
||||
# root标记位结束
|
||||
|
||||
# ssl标记位开始
|
||||
# ssl标记位结束
|
||||
|
||||
# php标记位开始
|
||||
include enable-php-%d.conf;
|
||||
# php标记位结束
|
||||
|
||||
# waf标记位开始
|
||||
waf on;
|
||||
waf_rule_path /www/server/openresty/ngx_waf/assets/rules/;
|
||||
waf_mode DYNAMIC;
|
||||
waf_cc_deny rate=1000r/m duration=60m;
|
||||
waf_cache capacity=50;
|
||||
# waf标记位结束
|
||||
|
||||
# 错误页配置,可自行设置
|
||||
#error_page 404 /404.html;
|
||||
#error_page 502 /502.html;
|
||||
|
||||
# 伪静态规则引入,修改后将导致面板设置的伪静态规则失效
|
||||
include /www/server/vhost/rewrite/%s.conf;
|
||||
|
||||
# 面板默认禁止访问部分敏感目录,可自行修改
|
||||
location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn)
|
||||
{
|
||||
return 404;
|
||||
}
|
||||
# 面板默认不记录静态资源的访问日志并开启1小时浏览器缓存,可自行修改
|
||||
location ~ .*\.(js|css)$
|
||||
{
|
||||
expires 1h;
|
||||
error_log /dev/null;
|
||||
access_log /dev/null;
|
||||
}
|
||||
|
||||
access_log /www/wwwlogs/%s.log;
|
||||
error_log /www/wwwlogs/%s.log;
|
||||
}
|
||||
|
||||
`, website.Path, website.Php, website.Name, website.Name, website.Name)
|
||||
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644)
|
||||
tools.Write("/www/server/vhost/rewrite"+website.Name+".conf", "", 0644)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
|
||||
// Status 网站状态
|
||||
func (c *WebsiteController) Status(ctx http.Context) http.Response {
|
||||
check := Check(ctx, "openresty")
|
||||
if check != nil {
|
||||
return check
|
||||
}
|
||||
id := ctx.Request().InputInt("id")
|
||||
if id == 0 {
|
||||
return Error(ctx, http.StatusBadRequest, "参数错误")
|
||||
}
|
||||
|
||||
website := models.Website{}
|
||||
if err := facades.Orm().Query().Where("id", id).Get(&website); err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 获取网站信息失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
website.Status = ctx.Request().InputBool("status")
|
||||
if err := facades.Orm().Query().Save(&website); err != nil {
|
||||
facades.Log().Error("[面板][WebsiteController] 保存网站配置失败 ", err)
|
||||
return Error(ctx, http.StatusInternalServerError, "系统内部错误")
|
||||
}
|
||||
|
||||
raw := tools.Read("/www/server/vhost/" + website.Name + ".conf")
|
||||
|
||||
// 运行目录
|
||||
rootConfig := tools.Cut(raw, "# root标记位开始\n", "# root标记位结束")
|
||||
match := regexp.MustCompile(`root\s+(.+);`).FindStringSubmatch(rootConfig)
|
||||
if len(match) == 2 {
|
||||
if website.Status {
|
||||
root := regexp.MustCompile(`# root\s+(.+);`).FindStringSubmatch(rootConfig)
|
||||
raw = strings.ReplaceAll(raw, rootConfig, " root "+root[1]+";\n ")
|
||||
} else {
|
||||
raw = strings.ReplaceAll(raw, rootConfig, " root /www/server/openresty/html;\n # root "+match[1]+";\n ")
|
||||
}
|
||||
}
|
||||
|
||||
// 默认文件
|
||||
indexConfig := tools.Cut(raw, "# index标记位开始\n", "# index标记位结束")
|
||||
match = regexp.MustCompile(`index\s+(.+);`).FindStringSubmatch(indexConfig)
|
||||
if len(match) == 2 {
|
||||
if website.Status {
|
||||
index := regexp.MustCompile(`# index\s+(.+);`).FindStringSubmatch(indexConfig)
|
||||
raw = strings.ReplaceAll(raw, indexConfig, " index "+index[1]+";\n ")
|
||||
} else {
|
||||
raw = strings.ReplaceAll(raw, indexConfig, " index stop.html;\n # index "+match[1]+";\n ")
|
||||
}
|
||||
}
|
||||
|
||||
tools.Write("/www/server/vhost/"+website.Name+".conf", raw, 0644)
|
||||
tools.Exec("systemctl reload openresty")
|
||||
|
||||
return Success(ctx, nil)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
|
||||
"panel/app/http/middleware"
|
||||
)
|
||||
|
||||
type Kernel struct {
|
||||
}
|
||||
|
||||
// The application's global HTTP middleware stack.
|
||||
// These middleware are run during every request to your application.
|
||||
func (kernel Kernel) Middleware() []http.Middleware {
|
||||
return []http.Middleware{
|
||||
middleware.Static(),
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/goravel/framework/auth"
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/facades"
|
||||
)
|
||||
|
||||
// Jwt 确保通过 JWT 鉴权
|
||||
func Jwt() http.Middleware {
|
||||
return func(ctx http.Context) {
|
||||
token := ctx.Request().Header("Authorization", ctx.Request().Header("Sec-WebSocket-Protocol"))
|
||||
if len(token) == 0 {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
|
||||
"code": 401,
|
||||
"message": "未登录",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// JWT 鉴权
|
||||
if _, err := facades.Auth().Parse(ctx, token); err != nil {
|
||||
if errors.Is(err, auth.ErrorTokenExpired) {
|
||||
token, err = facades.Auth().Refresh(ctx)
|
||||
if err != nil {
|
||||
// 到达刷新时间上限
|
||||
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
|
||||
"code": 401,
|
||||
"message": "登录已过期",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
token = "Bearer " + token
|
||||
} else {
|
||||
ctx.Request().AbortWithStatusJson(http.StatusOK, http.Json{
|
||||
"code": 401,
|
||||
"message": "登录已过期",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Response().Header("Authorization", token)
|
||||
ctx.Request().Next()
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-contrib/static"
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/gin"
|
||||
|
||||
"panel/app/services"
|
||||
)
|
||||
|
||||
func Static() http.Middleware {
|
||||
return func(ctx http.Context) {
|
||||
// 自动纠正 URL 格式
|
||||
if ctx.Request().Path() == services.NewSettingImpl().Get("entrance", "/") && ctx.Request().Path() != "/" {
|
||||
// ctx.Response().Redirect(http.StatusFound, ctx.Request().Path()+"/")
|
||||
ctx.Response().Writer().WriteHeader(http.StatusFound)
|
||||
_, err := ctx.Response().Writer().Write([]byte(fmt.Sprintf(`<html><head><meta http-equiv="refresh" content="0;url=%s/"></head></html>`, ctx.Request().Path())))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ctx.Response().Flush()
|
||||
return
|
||||
}
|
||||
|
||||
static.Serve(services.NewSettingImpl().Get("entrance", "/"), static.LocalFile("public", false))(ctx.(*gin.Context).Instance())
|
||||
ctx.Request().Next()
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package requests
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/http"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
)
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
|
||||
CaptchaID string `json:"captcha_id" form:"captcha_id"`
|
||||
Captcha string `json:"captcha" form:"captcha"`
|
||||
}
|
||||
|
||||
func (r *LoginRequest) Authorize(ctx http.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LoginRequest) Rules(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"username": "required",
|
||||
"password": "required|min_len:8",
|
||||
"captcha": "captcha:true",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LoginRequest) Messages(ctx http.Context) map[string]string {
|
||||
return map[string]string{
|
||||
"username.required": "登录名不能为空",
|
||||
"password.required": "密码不能为空",
|
||||
"password.min_len": "密码长度不能小于 8 位",
|
||||
"captcha.captcha": "验证码错误",
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LoginRequest) Attributes(ctx http.Context) map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (r *LoginRequest) PrepareForValidation(ctx http.Context, data validation.Data) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package jobs
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/models"
|
||||
)
|
||||
|
||||
// ProcessTask 处理面板任务
|
||||
type ProcessTask struct {
|
||||
}
|
||||
|
||||
// Signature The name and signature of the job.
|
||||
func (receiver *ProcessTask) Signature() string {
|
||||
return "process_task"
|
||||
}
|
||||
|
||||
// Handle Execute the job.
|
||||
func (receiver *ProcessTask) Handle(args ...any) error {
|
||||
taskID, ok := args[0].(uint)
|
||||
if !ok {
|
||||
facades.Log().Error("[面板][ProcessTask] 任务ID参数错误")
|
||||
return nil
|
||||
}
|
||||
|
||||
for {
|
||||
if !haveRunningTask() {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
var task models.Task
|
||||
if err := facades.Orm().Query().Where("id = ?", taskID).Get(&task); err != nil {
|
||||
facades.Log().Errorf("[面板][ProcessTask] 获取任务%d失败: %s", taskID, err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
task.Status = models.TaskStatusRunning
|
||||
if err := facades.Orm().Query().Save(&task); err != nil {
|
||||
facades.Log().Errorf("[面板][ProcessTask] 更新任务%d失败: %s", taskID, err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
facades.Log().Infof("[面板][ProcessTask] 开始执行任务%d", taskID)
|
||||
cmd := exec.Command("bash", "-c", task.Shell)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
task.Status = models.TaskStatusFailed
|
||||
if err := facades.Orm().Query().Save(&task); err != nil {
|
||||
facades.Log().Errorf("[面板][ProcessTask] 更新任务%d失败: %s", taskID, err.Error())
|
||||
return nil
|
||||
}
|
||||
facades.Log().Errorf("[面板][ProcessTask] 任务%d执行失败: %s", taskID, err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
task.Status = models.TaskStatusSuccess
|
||||
if err := facades.Orm().Query().Save(&task); err != nil {
|
||||
facades.Log().Errorf("[面板][ProcessTask] 更新任务%d失败: %s", taskID, err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
facades.Log().Infof("[面板][ProcessTask] 任务%d执行成功", taskID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// haveRunningTask 是否有任务正在执行
|
||||
func haveRunningTask() bool {
|
||||
var task models.Task
|
||||
if err := facades.Orm().Query().Where("status = ?", models.TaskStatusRunning).Get(&task); err != nil {
|
||||
facades.Log().Error("[面板][ProcessTask] 获取任务失败: " + err.Error())
|
||||
return true
|
||||
}
|
||||
|
||||
if task.ID != 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
)
|
||||
|
||||
type Cron struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;unique" json:"name"`
|
||||
Status bool `gorm:"not null;default:false" json:"status"`
|
||||
Type string `gorm:"not null" json:"type"`
|
||||
Time string `gorm:"not null" json:"time"`
|
||||
Shell string `gorm:"default:''" json:"shell"`
|
||||
Log string `gorm:"default:''" json:"log"`
|
||||
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package models
|
||||
|
||||
import "github.com/goravel/framework/support/carbon"
|
||||
|
||||
type Database struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"unique;not null" json:"name"`
|
||||
Type string `gorm:"not null;index" json:"type"`
|
||||
Host string `gorm:"not null" json:"host"`
|
||||
Port int `gorm:"not null" json:"port"`
|
||||
Username string `gorm:"not null" json:"username"`
|
||||
Password string `gorm:"default:''" json:"password"`
|
||||
Remark string `gorm:"default:''" json:"remark"`
|
||||
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
|
||||
"panel/pkg/tools"
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Info tools.MonitoringInfo `gorm:"type:json;serializer:json" json:"info"`
|
||||
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package models
|
||||
|
||||
import "github.com/goravel/framework/support/carbon"
|
||||
|
||||
type Plugin struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Slug string `gorm:"unique;not null" json:"slug"`
|
||||
Version string `gorm:"not null" json:"version"`
|
||||
Show bool `gorm:"default:false;not null" json:"show"`
|
||||
ShowOrder int `gorm:"default:0;not null" json:"show_order"`
|
||||
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package models
|
||||
|
||||
import "github.com/goravel/framework/support/carbon"
|
||||
|
||||
const (
|
||||
SettingKeyName = "name"
|
||||
SettingKeyVersion = "version"
|
||||
SettingKeyMonitor = "monitor"
|
||||
SettingKeyMonitorDays = "monitor_days"
|
||||
SettingKeyBackupPath = "backup_path"
|
||||
SettingKeyWebsitePath = "website_path"
|
||||
SettingKeyEntrance = "entrance"
|
||||
SettingKeyMysqlRootPassword = "mysql_root_password"
|
||||
SettingKeySshHost = "ssh_host"
|
||||
SettingKeySshPort = "ssh_port"
|
||||
SettingKeySshUser = "ssh_user"
|
||||
SettingKeySshPassword = "ssh_password"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Key string `gorm:"unique;not null" json:"key"`
|
||||
Value string `gorm:"default:''" json:"value"`
|
||||
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
)
|
||||
|
||||
const (
|
||||
TaskStatusWaiting = "waiting"
|
||||
TaskStatusRunning = "running"
|
||||
TaskStatusSuccess = "finished"
|
||||
TaskStatusFailed = "failed"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Status string `gorm:"not null;default:'waiting'" json:"status"`
|
||||
Shell string `gorm:"default:''" json:"shell"`
|
||||
Log string `gorm:"default:''" json:"log"`
|
||||
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package models
|
||||
|
||||
import "github.com/goravel/framework/support/carbon"
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"unique;not null" json:"username"`
|
||||
Password string `gorm:"not null" json:"password"`
|
||||
Email string `gorm:"default:''" json:"email"`
|
||||
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/support/carbon"
|
||||
)
|
||||
|
||||
type Website struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"unique;not null" json:"name"`
|
||||
Status bool `gorm:"default:true;not null;index" json:"status"`
|
||||
Path string `gorm:"not null" json:"path"`
|
||||
Php int `gorm:"default:0;not null;index" json:"php"`
|
||||
Ssl bool `gorm:"default:false;not null;index" json:"ssl"`
|
||||
Remark string `gorm:"default:''" json:"remark"`
|
||||
CreatedAt carbon.DateTime `gorm:"autoCreateTime;column:created_at" json:"created_at"`
|
||||
UpdatedAt carbon.DateTime `gorm:"autoUpdateTime;column:updated_at" json:"updated_at"`
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package fail2ban
|
||||
|
||||
var (
|
||||
Name = "Fail2ban"
|
||||
Description = "Fail2ban 扫描系统日志文件并从中找出多次尝试失败的IP地址,将该IP地址加入防火墙的拒绝访问列表中。"
|
||||
Slug = "fail2ban"
|
||||
Version = "1.0.0"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/fail2ban/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/fail2ban/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/fail2ban/update.sh`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package mysql57
|
||||
|
||||
var (
|
||||
Name = "MySQL-5.7"
|
||||
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。"
|
||||
Slug = "mysql57"
|
||||
Version = "5.7.43"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"mysql80"}
|
||||
Install = `bash /www/panel/scripts/mysql/install.sh 57`
|
||||
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 57`
|
||||
Update = `bash /www/panel/scripts/mysql/update.sh 57`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package mysql80
|
||||
|
||||
var (
|
||||
Name = "MySQL-8.0"
|
||||
Description = "MySQL 是最流行的关系型数据库管理系统之一,Oracle 旗下产品。(内存 < 4G 无法安装)"
|
||||
Slug = "mysql80"
|
||||
Version = "8.0.34"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"mysql57"}
|
||||
Install = `bash /www/panel/scripts/mysql/install.sh 80`
|
||||
Uninstall = `bash /www/panel/scripts/mysql/uninstall.sh 80`
|
||||
Update = `bash /www/panel/scripts/mysql/update.sh 80`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package openresty
|
||||
|
||||
var (
|
||||
Name = "OpenResty"
|
||||
Description = "OpenResty® 是一款基于 NGINX 和 LuaJIT 的 Web 平台。"
|
||||
Slug = "openresty"
|
||||
Version = "1.21.4.2"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = "bash /www/panel/scripts/openresty/install.sh"
|
||||
Uninstall = "bash /www/panel/scripts/openresty/uninstall.sh"
|
||||
Update = "bash /www/panel/scripts/openresty/install.sh"
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package php74
|
||||
|
||||
var (
|
||||
Name = "PHP-7.4"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php74"
|
||||
Version = "7.4.33"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/php/install.sh 74`
|
||||
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 74`
|
||||
Update = `bash /www/panel/scripts/php/install.sh 74`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package php80
|
||||
|
||||
var (
|
||||
Name = "PHP-8.0"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php80"
|
||||
Version = "8.0.30"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/php/install.sh 80`
|
||||
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 80`
|
||||
Update = `bash /www/panel/scripts/php/install.sh 80`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package php81
|
||||
|
||||
var (
|
||||
Name = "PHP-8.1"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php81"
|
||||
Version = "8.1.23"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/php/install.sh 81`
|
||||
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 81`
|
||||
Update = `bash /www/panel/scripts/php/install.sh 81`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package php82
|
||||
|
||||
var (
|
||||
Name = "PHP-8.2"
|
||||
Description = "PHP 是世界上最好的语言!"
|
||||
Slug = "php82"
|
||||
Version = "8.2.10"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/php/install.sh 82`
|
||||
Uninstall = `bash /www/panel/scripts/php/uninstall.sh 82`
|
||||
Update = `bash /www/panel/scripts/php/install.sh 82`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package phpmyadmin
|
||||
|
||||
var (
|
||||
Name = "phpMyAdmin"
|
||||
Description = "phpMyAdmin 是一个以 PHP 为基础,以 Web-Base 方式架构在网站主机上的 MySQL 数据库管理工具。"
|
||||
Slug = "phpmyadmin"
|
||||
Version = "5.2.1"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/phpmyadmin/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/phpmyadmin/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/phpmyadmin/uninstall.sh && bash /www/panel/scripts/phpmyadmin/install.sh`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package postgresql15
|
||||
|
||||
var (
|
||||
Name = "PostgreSQL-15"
|
||||
Description = "PostgreSQL 是世界上最先进的开源关系数据库,在类似 BSD 与 MIT 许可的 PostgreSQL 许可下发行。"
|
||||
Slug = "postgresql15"
|
||||
Version = "15.4"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"postgresql16"}
|
||||
Install = `bash /www/panel/scripts/postgresql/install.sh 15`
|
||||
Uninstall = `bash /www/panel/scripts/postgresql/uninstall.sh 15`
|
||||
Update = `bash /www/panel/scripts/postgresql/update.sh 15`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package postgresql16
|
||||
|
||||
var (
|
||||
Name = "PostgreSQL-16"
|
||||
Description = "PostgreSQL 是世界上最先进的开源关系数据库,在类似 BSD 与 MIT 许可的 PostgreSQL 许可下发行。"
|
||||
Slug = "postgresql16"
|
||||
Version = "16.0"
|
||||
Requires = []string{}
|
||||
Excludes = []string{"postgresql15"}
|
||||
Install = `bash /www/panel/scripts/postgresql/install.sh 16`
|
||||
Uninstall = `bash /www/panel/scripts/postgresql/uninstall.sh 16`
|
||||
Update = `bash /www/panel/scripts/postgresql/update.sh 16`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package pureftpd
|
||||
|
||||
var (
|
||||
Name = "Pure-FTPd"
|
||||
Description = "Pure-Ftpd 是一个快速、高效、轻便、安全的 FTP 服务器,它以安全和配置简单为设计目标,支持虚拟主机,IPV6,PAM 等功能。"
|
||||
Slug = "pureftpd"
|
||||
Version = "1.0.50"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/pureftpd/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/pureftpd/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/pureftpd/update.sh`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package redis
|
||||
|
||||
var (
|
||||
Name = "Redis"
|
||||
Description = "Redis 是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。"
|
||||
Slug = "redis"
|
||||
Version = "7.0.12"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/redis/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/redis/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/redis/update.sh`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package s3fs
|
||||
|
||||
var (
|
||||
Name = "S3fs"
|
||||
Description = "S3fs 通过 FUSE 挂载兼容 S3 标准的存储桶,例如Amazon S3、阿里云OSS、腾讯云COS、七牛云Kodo等。"
|
||||
Slug = "s3fs"
|
||||
Version = "1.9"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/s3fs/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/s3fs/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/s3fs/install.sh`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package supervisor
|
||||
|
||||
var (
|
||||
Name = "Supervisor"
|
||||
Description = "Supervisor 是一个客户端/服务器系统,允许用户监视和控制类 UNIX 操作系统上的多个进程。"
|
||||
Slug = "supervisor"
|
||||
Version = "4.2.5"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `bash /www/panel/scripts/supervisor/install.sh`
|
||||
Uninstall = `bash /www/panel/scripts/supervisor/uninstall.sh`
|
||||
Update = `bash /www/panel/scripts/supervisor/install.sh`
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
package toolbox
|
||||
|
||||
var (
|
||||
Name = "系统工具箱"
|
||||
Description = "可视化调整一些常用的配置项,如 DNS、SWAP、时区 等"
|
||||
Slug = "toolbox"
|
||||
Version = "1.0"
|
||||
Requires = []string{}
|
||||
Excludes = []string{}
|
||||
Install = `panel writePlugin toolbox 1.0`
|
||||
Uninstall = `panel deletePlugin toolbox`
|
||||
Update = `panel writePlugin toolbox 1.0`
|
||||
)
|
||||
@@ -1,16 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/foundation"
|
||||
)
|
||||
|
||||
type AppServiceProvider struct {
|
||||
}
|
||||
|
||||
func (receiver *AppServiceProvider) Register(app foundation.Application) {
|
||||
|
||||
}
|
||||
|
||||
func (receiver *AppServiceProvider) Boot(app foundation.Application) {
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/foundation"
|
||||
)
|
||||
|
||||
type AuthServiceProvider struct {
|
||||
}
|
||||
|
||||
func (receiver *AuthServiceProvider) Register(app foundation.Application) {
|
||||
|
||||
}
|
||||
|
||||
func (receiver *AuthServiceProvider) Boot(app foundation.Application) {
|
||||
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/foundation"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/console"
|
||||
)
|
||||
|
||||
type ConsoleServiceProvider struct {
|
||||
}
|
||||
|
||||
func (receiver *ConsoleServiceProvider) Register(app foundation.Application) {
|
||||
kernel := console.Kernel{}
|
||||
facades.Schedule().Register(kernel.Schedule())
|
||||
facades.Artisan().Register(kernel.Commands())
|
||||
}
|
||||
|
||||
func (receiver *ConsoleServiceProvider) Boot(app foundation.Application) {
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/database/seeder"
|
||||
"github.com/goravel/framework/contracts/foundation"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/database/seeders"
|
||||
)
|
||||
|
||||
type DatabaseServiceProvider struct {
|
||||
}
|
||||
|
||||
func (receiver *DatabaseServiceProvider) Register(app foundation.Application) {
|
||||
|
||||
}
|
||||
|
||||
func (receiver *DatabaseServiceProvider) Boot(app foundation.Application) {
|
||||
facades.Seeder().Register([]seeder.Seeder{
|
||||
&seeders.DatabaseSeeder{},
|
||||
})
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/event"
|
||||
"github.com/goravel/framework/contracts/foundation"
|
||||
"github.com/goravel/framework/facades"
|
||||
)
|
||||
|
||||
type EventServiceProvider struct {
|
||||
}
|
||||
|
||||
func (receiver *EventServiceProvider) Register(app foundation.Application) {
|
||||
facades.Event().Register(receiver.listen())
|
||||
}
|
||||
|
||||
func (receiver *EventServiceProvider) Boot(app foundation.Application) {
|
||||
|
||||
}
|
||||
|
||||
func (receiver *EventServiceProvider) listen() map[event.Event][]event.Listener {
|
||||
return map[event.Event][]event.Listener{}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/foundation"
|
||||
"github.com/goravel/framework/contracts/queue"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/jobs"
|
||||
)
|
||||
|
||||
type QueueServiceProvider struct {
|
||||
}
|
||||
|
||||
func (receiver *QueueServiceProvider) Register(app foundation.Application) {
|
||||
facades.Queue().Register(receiver.Jobs())
|
||||
}
|
||||
|
||||
func (receiver *QueueServiceProvider) Boot(app foundation.Application) {
|
||||
|
||||
}
|
||||
|
||||
func (receiver *QueueServiceProvider) Jobs() []queue.Job {
|
||||
return []queue.Job{
|
||||
&jobs.ProcessTask{},
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/foundation"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/http"
|
||||
"panel/routes"
|
||||
)
|
||||
|
||||
type RouteServiceProvider struct {
|
||||
}
|
||||
|
||||
func (receiver *RouteServiceProvider) Register(app foundation.Application) {
|
||||
}
|
||||
|
||||
func (receiver *RouteServiceProvider) Boot(app foundation.Application) {
|
||||
// Add HTTP middlewares
|
||||
facades.Route().GlobalMiddleware(http.Kernel{}.Middleware()...)
|
||||
|
||||
receiver.configureRateLimiting()
|
||||
|
||||
routes.Api()
|
||||
routes.Plugin()
|
||||
}
|
||||
|
||||
func (receiver *RouteServiceProvider) configureRateLimiting() {
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/foundation"
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"panel/app/rules"
|
||||
)
|
||||
|
||||
type ValidationServiceProvider struct {
|
||||
}
|
||||
|
||||
func (receiver *ValidationServiceProvider) Register(app foundation.Application) {
|
||||
|
||||
}
|
||||
|
||||
func (receiver *ValidationServiceProvider) Boot(app foundation.Application) {
|
||||
if err := facades.Validation().AddRules(receiver.rules()); err != nil {
|
||||
facades.Log().Errorf("add rules error: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (receiver *ValidationServiceProvider) rules() []validation.Rule {
|
||||
return []validation.Rule{
|
||||
&rules.Exists{},
|
||||
&rules.NotExists{},
|
||||
&rules.Captcha{},
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
|
||||
"panel/pkg/captcha"
|
||||
)
|
||||
|
||||
type Captcha struct {
|
||||
}
|
||||
|
||||
// Signature The name of the rule.
|
||||
func (receiver *Captcha) Signature() string {
|
||||
return "captcha"
|
||||
}
|
||||
|
||||
// Passes Determine if the validation rule passes.
|
||||
func (receiver *Captcha) Passes(data validation.Data, val any, options ...any) bool {
|
||||
captchaID, exist := data.Get("captcha_id")
|
||||
if !exist {
|
||||
return false
|
||||
}
|
||||
|
||||
// 第一个参数(如果有),是否清除验证码,如 false
|
||||
clear := true
|
||||
var err error
|
||||
if len(options) > 0 {
|
||||
clear, err = strconv.ParseBool(options[0].(string))
|
||||
if err != nil {
|
||||
clear = true
|
||||
}
|
||||
}
|
||||
|
||||
if !captcha.NewCaptcha().VerifyCaptcha(captchaID.(string), val.(string), clear) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Message Get the validation error message.
|
||||
func (receiver *Captcha) Message() string {
|
||||
return ""
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
"github.com/goravel/framework/facades"
|
||||
)
|
||||
|
||||
type Exists struct {
|
||||
}
|
||||
|
||||
// Signature The name of the rule.
|
||||
func (receiver *Exists) Signature() string {
|
||||
return "exists"
|
||||
}
|
||||
|
||||
// Passes Determine if the validation rule passes.
|
||||
func (receiver *Exists) Passes(_ validation.Data, val any, options ...any) bool {
|
||||
|
||||
// 第一个参数,表名称,如 categories
|
||||
tableName := options[0].(string)
|
||||
// 第二个参数,字段名称,如 id
|
||||
fieldName := options[1].(string)
|
||||
// 用户请求过来的数据
|
||||
requestValue, ok := val.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// 判断是否为空
|
||||
if len(requestValue) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 判断是否存在
|
||||
var count int64
|
||||
query := facades.Orm().Query().Table(tableName).Where(fieldName, requestValue)
|
||||
// 判断第三个参数及之后的参数是否存在
|
||||
if len(options) > 2 {
|
||||
for i := 2; i < len(options); i++ {
|
||||
query = query.OrWhere(options[i].(string), requestValue)
|
||||
}
|
||||
}
|
||||
err := query.Count(&count)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return count != 0
|
||||
}
|
||||
|
||||
// Message Get the validation error message.
|
||||
func (receiver *Exists) Message() string {
|
||||
return "记录不存在"
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"github.com/goravel/framework/contracts/validation"
|
||||
"github.com/goravel/framework/facades"
|
||||
)
|
||||
|
||||
type NotExists struct {
|
||||
}
|
||||
|
||||
// Signature The name of the rule.
|
||||
func (receiver *NotExists) Signature() string {
|
||||
return "not_exists"
|
||||
}
|
||||
|
||||
// Passes Determine if the validation rule passes.
|
||||
func (receiver *NotExists) Passes(_ validation.Data, val any, options ...any) bool {
|
||||
|
||||
// 第一个参数,表名称,如 categories
|
||||
tableName := options[0].(string)
|
||||
// 第二个参数,字段名称,如 id
|
||||
fieldName := options[1].(string)
|
||||
// 用户请求过来的数据
|
||||
requestValue, ok := val.(string)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// 判断是否为空
|
||||
if len(requestValue) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 判断是否存在
|
||||
var count int64
|
||||
query := facades.Orm().Query().Table(tableName).Where(fieldName, requestValue)
|
||||
// 判断第三个参数及之后的参数是否存在
|
||||
if len(options) > 2 {
|
||||
for i := 2; i < len(options); i++ {
|
||||
query = query.OrWhere(options[i].(string), requestValue)
|
||||
}
|
||||
}
|
||||
err := query.Count(&count)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return count == 0
|
||||
}
|
||||
|
||||
// Message Get the validation error message.
|
||||
func (receiver *NotExists) Message() string {
|
||||
return "记录已存在"
|
||||
}
|
||||