Compare commits
479 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ce41bc02c | |||
| 2f87de40da | |||
| b47bd5c14d | |||
| a0d92d594c | |||
| c8ebadfc4d | |||
| a3bbbfa87c | |||
| 8c7a65a8b6 | |||
| 4b9ce222ba | |||
| 29ad4f5f34 | |||
| a505a6be92 | |||
| d21a9f0559 | |||
| a5eee5eeec | |||
| fc87cd0360 | |||
| fc97709ebf | |||
| 7bc716cff6 | |||
| 128f44ca55 | |||
| 2ebe0ba47d | |||
| 1fee3632fc | |||
| f66ce68e36 | |||
| 599ea2c245 | |||
| 674c48cce3 | |||
| 18ba92f44b | |||
| e8f7e1af76 | |||
| 9ae19208cb | |||
| ee13b03748 | |||
| 1cbb42a008 | |||
| 4a7258009d | |||
| 84158b2398 | |||
| 460ab12af3 | |||
| 78de211e49 | |||
| 234180f160 | |||
| f25a809dc9 | |||
| 0651f0efc9 | |||
| 8d58adfab4 | |||
| 0b08e59b6e | |||
| 6a60895eac | |||
| 37c10e65a4 | |||
| 695318d96e | |||
| 40a57792ff | |||
| 7c75508b25 | |||
| 7da21e3d6a | |||
| e7ddedea76 | |||
| 7a656595d9 | |||
| 5087399b26 | |||
| 110b40edd7 | |||
| fec09652b7 | |||
| 1dc0042507 | |||
| 746d2297fe | |||
| 7736143928 | |||
| a680a46a22 | |||
| bdd1565221 | |||
| c0e32b46c5 | |||
| 03b3ea3a8b | |||
| 5efeceecdb | |||
| 52587e9cb3 | |||
| c2aa4a4904 | |||
| 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 |
@@ -1,4 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
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
|
||||
@@ -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,7 +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
|
||||
|
||||
@@ -25,7 +25,7 @@ body:
|
||||
attributes:
|
||||
label: 描述功能 (Describe Feature)
|
||||
description: |
|
||||
简明概要地描述你的新功能,以及它将解决什么问题。
|
||||
简明概要地描述您的新功能,以及它将解决什么问题。
|
||||
Briefly describe your new feature and what problem it will solve.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
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 |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 819 KiB |
|
After Width: | Height: | Size: 64 KiB |
@@ -14,6 +14,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
@@ -28,20 +30,37 @@ jobs:
|
||||
check-name: 'build (frontend)'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Download frontend
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v7
|
||||
with:
|
||||
workflow: frontend.yml
|
||||
name: frontend
|
||||
path: internal/embed/frontend
|
||||
check_artifacts: true
|
||||
- name: Set build info
|
||||
run: |
|
||||
echo "VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo '0.0.0')" >> $GITHUB_ENV
|
||||
echo "BUILD_TIME=$(date -u '+%F %T UTC')" >> $GITHUB_ENV
|
||||
echo "COMMIT_HASH=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
echo "GO_VERSION=$(go version | cut -d' ' -f3)" >> $GITHUB_ENV
|
||||
echo "BUILD_ID=${{ github.run_id }}" >> $GITHUB_ENV
|
||||
echo "BUILD_USER=$(whoami)" >> $GITHUB_ENV
|
||||
echo "BUILD_HOST=$(hostname)" >> $GITHUB_ENV
|
||||
- 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
|
||||
LDFLAGS="-s -w --extldflags '-static'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/TheTNB/panel/internal/app.Version=${VERSION}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/TheTNB/panel/internal/app.BuildTime=${BUILD_TIME}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/TheTNB/panel/internal/app.CommitHash=${COMMIT_HASH}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/TheTNB/panel/internal/app.GoVersion=${GO_VERSION}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/TheTNB/panel/internal/app.BuildID=${BUILD_ID}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/TheTNB/panel/internal/app.BuildUser=${BUILD_USER}'"
|
||||
LDFLAGS="${LDFLAGS} -X 'github.com/TheTNB/panel/internal/app.BuildHost=${BUILD_HOST}'"
|
||||
go build -ldflags "${LDFLAGS}" -o web-${{ matrix.goarch }} ./cmd/web
|
||||
go build -ldflags "${LDFLAGS}" -o cli-${{ matrix.goarch }} ./cmd/cli
|
||||
- name: Compress ${{ matrix.goarch }}
|
||||
run: |
|
||||
upx --best --lzma web-${{ matrix.goarch }}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
name: Codecov
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
jobs:
|
||||
codecov:
|
||||
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 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@v4
|
||||
with:
|
||||
file: ./coverage.out
|
||||
token: ${{ secrets.CODECOV }}
|
||||
@@ -23,14 +23,14 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
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.ts.example settings/proxy-config.ts
|
||||
cp settings/proxy-config.example.ts settings/proxy-config.ts
|
||||
pnpm dev &
|
||||
sleep 5
|
||||
kill %1
|
||||
|
||||
@@ -25,12 +25,16 @@ jobs:
|
||||
check-name: 'build (frontend)'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Download frontend
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v7
|
||||
with:
|
||||
workflow: frontend.yml
|
||||
name: frontend
|
||||
path: internal/embed/frontend
|
||||
check_artifacts: true
|
||||
- name: Set environment variables
|
||||
run: |
|
||||
echo "GOVERSION=$(go version | cut -d' ' -f3)" >> $GITHUB_ENV
|
||||
echo "HOSTNAME=$(hostname)" >> $GITHUB_ENV
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
|
||||
@@ -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"
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: web/pnpm-lock.yaml
|
||||
- name: Run pnpm lint
|
||||
|
||||
@@ -21,4 +21,9 @@ jobs:
|
||||
run: |
|
||||
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 }}
|
||||
@@ -14,6 +14,13 @@ builds:
|
||||
- arm64
|
||||
ldflags:
|
||||
- -s -w --extldflags "-static"
|
||||
- -X 'github.com/TheTNB/panel/internal/app.Version={{ .Version }}'
|
||||
- -X 'github.com/TheTNB/panel/internal/app.BuildTime={{ .Now.Format "2006-01-02 15:04:05 MST" }}'
|
||||
- -X 'github.com/TheTNB/panel/internal/app.CommitHash={{ .ShortCommit }}'
|
||||
- -X 'github.com/TheTNB/panel/internal/app.GoVersion={{ .Env.GOVERSION }}'
|
||||
- -X 'github.com/TheTNB/panel/internal/app.BuildID={{ .Env.GITHUB_RUN_ID }}'
|
||||
- -X 'github.com/TheTNB/panel/internal/app.BuildUser={{ .Env.USER }}'
|
||||
- -X 'github.com/TheTNB/panel/internal/app.BuildHost={{ .Env.HOSTNAME }}'
|
||||
- id: cli
|
||||
main: ./cmd/cli
|
||||
binary: cli
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
|
||||
<h1 align="center" style="font-size: 40px">耗子面板</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/10950" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10950" alt="TheTNB%2Fpanel | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||
<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>
|
||||
@@ -16,9 +12,9 @@
|
||||
<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>
|
||||
|
||||
耗子面板是使用 Golang + Vue 开发的开源轻量 Linux 服务器运维管理面板。
|
||||
耗子面板是新一代企业级全能服务器运维管理面板。简单轻量,高效运维。
|
||||
|
||||
交流QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 论坛:[tom.moe](https://tom.moe) | 赞助:[爱发电](https://afdian.com/a/TheTNB)
|
||||
QQ群:[12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | 微信群:[复制此链接](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | 论坛:[bbs.haozi.net](https://bbs.haozi.net)
|
||||
|
||||
## 优势
|
||||
|
||||
@@ -26,13 +22,13 @@
|
||||
2. **低破坏性:** 面板的设计理念是尽可能减少对系统的额外修改,在同类面板中,我们对系统的修改最少。
|
||||
3. **追随时代:** 面板所有组件均走在时代前沿,更新快,功能强大,安全性有保障。
|
||||
4. **高效运维:** 面板界面简洁,操作简单,无需繁琐的配置,即可快速部署各类环境、调整应用设置。
|
||||
5. **离线运行:** 面板运行可不依赖任何外部服务,你甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
|
||||
5. **离线运行:** 面板运行可不依赖任何外部服务,您甚至可以在部署完成后停止面板进程,不会对已部署服务造成任何影响。
|
||||
6. **久经考验:** 我们生产环境自 2022 年即开始使用,已稳定运行 2 年无事故。
|
||||
7. **开源开放:** 面板开源,你可以自由修改、审计面板源码,安全性有保障。
|
||||
7. **开源开放:** 面板开源,您可以自由修改、审计面板源码,安全性有保障。
|
||||
|
||||
## UI 截图
|
||||
|
||||

|
||||

|
||||
|
||||
## 运行环境
|
||||
|
||||
@@ -46,30 +42,37 @@
|
||||
|---------------------|-----|-----|
|
||||
| AlmaLinux | 9 | 推荐 |
|
||||
| AlmaLinux | 8 | 不推荐 |
|
||||
| RockyLinux | 9 | |
|
||||
| RockyLinux | 9 | 支持 |
|
||||
| RockyLinux | 8 | 不推荐 |
|
||||
| CentOS Stream | 9 | 不推荐 |
|
||||
| CentOS Stream | 8 | 不推荐 |
|
||||
| Ubuntu | 24 | 推荐 |
|
||||
| Ubuntu | 22 | |
|
||||
| Ubuntu | 22 | 支持 |
|
||||
| Debian | 12 | 推荐 |
|
||||
| Debian | 11 | |
|
||||
| OpenCloudOS | 9 | |
|
||||
| TencentOS Server | 4 | |
|
||||
| Debian | 11 | 支持 |
|
||||
| OpenCloudOS | 9 | 支持 |
|
||||
| TencentOS Server | 4 | 支持 |
|
||||
| TencentOS Server | 3.1 | 不推荐 |
|
||||
| Alibaba Cloud Linux | 3.2 | 不推荐 |
|
||||
| Anolis | 8 | 不推荐 |
|
||||
| openEuler | 22 | 不推荐 |
|
||||
|
||||
随着系统版本的不断更新,我们亦可能会终止部分过于老旧的系统的支持,以保证面板的健壮性。
|
||||
|
||||
## 挂载硬盘
|
||||
|
||||
如果您的服务器有未挂载的数据盘,可在安装前以`root`用户登录服务器运行以下命令自动挂载,面板安装后不支持跨目录迁移。
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o auto_mount.sh https://dl.cdn.haozi.net/panel/auto_mount.sh && bash auto_mount.sh
|
||||
```
|
||||
|
||||
## 安装面板
|
||||
|
||||
> **Warning**
|
||||
> 安装面板前,你需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
|
||||
> 安装面板前,您需要了解 LNMP 环境的基本知识,以及如何处理常见的 LNMP 环境问题,我们不建议 0 基础的用户安装和使用耗子面板。
|
||||
|
||||
如果你决定继续,请以`root`用户登录服务器,执行以下命令安装面板:
|
||||
|
||||
**当前 v2.3.x 为测试版本,欢迎帮助我们测试在不同操作系统下的兼容性。**
|
||||
以`root`用户登录服务器,运行以下命令安装面板:
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
|
||||
@@ -79,7 +82,7 @@ curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash in
|
||||
|
||||
优先建议备份数据重装系统,这样可以保证系统纯净。
|
||||
|
||||
如果你无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
|
||||
如果您无法重装系统,请以`root`用户登录服务器,执行以下命令卸载面板:
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bash uninstall.sh
|
||||
@@ -95,29 +98,51 @@ curl -fsLm 10 -o uninstall.sh https://dl.cdn.haozi.net/panel/uninstall.sh && bas
|
||||
panel-cli
|
||||
```
|
||||
|
||||
在 [文档](https://tom.moe/docs?category=57) 中查看更多使用方法和技巧。
|
||||
在 [文档](https://bbs.haozi.net/docs?category=57) 中查看更多使用方法和技巧。
|
||||
|
||||
## 问题反馈
|
||||
|
||||
使用类问题,可在 [Moe Tom](https://tom.moe) 提问或寻求 AI 帮助,亦可在群里寻求付费支持。
|
||||
使用类问题,可在 [Moe Tom](https://bbs.haozi.net) 提问或寻求 AI 帮助,亦可在群里寻求付费支持。
|
||||
|
||||
面板自身问题,可在 GitHub 的`Issues`页面提交问题反馈,注意[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)。
|
||||
|
||||
## 赞助商
|
||||
|
||||
如果耗子面板对你有帮助,欢迎[赞助我们](https://afdian.com/a/TheTNB),感谢以下支持者/赞助商的支持:
|
||||
如果耗子面板对您有帮助,欢迎赞助我们,同时感谢以下支持者/赞助商的支持:
|
||||
|
||||
**同时接受云资源赞助,可通过QQ群咨询联系**
|
||||
 
|
||||
|
||||
### 资金
|
||||
|
||||
- [微晓朵](https://www.weixiaoduo.com/)
|
||||
<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)
|
||||
- [WAF PRO](https://waf.pro/)
|
||||
- [盾云SCDN](https://scdn.ddunyun.com/)
|
||||
<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>
|
||||
|
||||
### 合作伙伴
|
||||
|
||||
<a href="https://1ms.run">
|
||||
<img height="80" src=".github/assets/1ms.svg" alt="毫秒镜像提供经过审核的 Docker 镜像加速服务">
|
||||
</a>
|
||||
|
||||
### 赞助商们
|
||||
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://afdian.com/a/TheTNB">
|
||||
@@ -129,18 +154,23 @@ panel-cli
|
||||
|
||||
这个项目的存在要归功于所有做出贡献的人,参与贡献请先查看贡献代码部分。
|
||||
|
||||
<a href="https://github.com/TheTNB/panel/graphs/contributors">
|
||||
<img alt="contributors" src="https://contrib.rocks/image?repo=TheTNB/panel"/>
|
||||
</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/#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>
|
||||
<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>
|
||||
|
||||
## 免责声明
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
|
||||
<h1 align="center" style="font-size: 40px">Rat Panel</h1>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/10950" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10950" alt="TheTNB%2Fpanel | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||
<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>
|
||||
@@ -16,9 +12,9 @@
|
||||
<a href="https://app.fossa.com/projects/git%2Bgithub.com%2FTheTNB%2Fpanel?ref=badge_shield"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2FTheTNB%2Fpanel.svg?type=shield" alt="FOSSA Status"></a>
|
||||
</p>
|
||||
|
||||
The Rat Panel is an open source lightweight Linux server operation and maintenance management panel developed using Golang and Vue.
|
||||
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.
|
||||
|
||||
Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | Forum: [tom.moe](https://tom.moe) | Sponsor: [Open Collective](https://opencollective.com/tnb)
|
||||
QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | WeChat group: [Copy this link](https://work.weixin.qq.com/gm/d8ebf618553398d454e3378695c858b6) | Forum: [bbs.haozi.net](https://bbs.haozi.net)
|
||||
|
||||
## Advantages
|
||||
|
||||
@@ -32,7 +28,7 @@ Communication QQ group: [12370907](https://jq.qq.com/?_wv=1027&k=I1oJKSTH) | For
|
||||
|
||||
## UI Screenshots
|
||||
|
||||

|
||||

|
||||
|
||||
## Operating Environment
|
||||
|
||||
@@ -46,30 +42,37 @@ For other systems not listed in the table below, you can try to install them by
|
||||
|---------------------|---------|-----------------|
|
||||
| AlmaLinux | 9 | Recommended |
|
||||
| AlmaLinux | 8 | Not recommended |
|
||||
| RockyLinux | 9 | |
|
||||
| RockyLinux | 9 | Support |
|
||||
| RockyLinux | 8 | Not recommended |
|
||||
| CentOS Stream | 9 | Not recommended |
|
||||
| CentOS Stream | 8 | Not recommended |
|
||||
| Ubuntu | 24 | Recommended |
|
||||
| Ubuntu | 22 | |
|
||||
| Ubuntu | 22 | Support |
|
||||
| Debian | 12 | Recommended |
|
||||
| Debian | 11 | |
|
||||
| OpenCloudOS | 9 | |
|
||||
| TencentOS Server | 4 | |
|
||||
| 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 |
|
||||
|
||||
As system versions are constantly updated, we may also terminate support for some older systems to ensure the robustness 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
|
||||
|
||||
> **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:
|
||||
|
||||
**Current v2.3.x is a test version, welcome to help us test compatibility on different operating systems.**
|
||||
Login to the server as the `root` user and run the following command to install the panel:
|
||||
|
||||
```shell
|
||||
curl -fsLm 10 -o install.sh https://dl.cdn.haozi.net/panel/install.sh && bash install.sh
|
||||
@@ -95,29 +98,51 @@ Use `panel-cli` command for daily maintenance:
|
||||
panel-cli
|
||||
```
|
||||
|
||||
See more usage methods and tips in [Document](https://tom.moe/docs?category=57).
|
||||
See more usage methods and tips in [Document](https://bbs.haozi.net/docs?category=57).
|
||||
|
||||
## Feedback
|
||||
|
||||
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 usage issues, you can ask questions in the [Moe Tom](https://bbs.haozi.net) 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).
|
||||
|
||||
## Sponsor
|
||||
|
||||
If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollective.com/tnb), thanks to the following supporters/sponsors:
|
||||
If the Rat Panel is helpful to you, welcome to sponsor us, also thanks to the following supporters/sponsors:
|
||||
|
||||
**Accept cloud resources and financial sponsorship, you can contact us through QQ group**
|
||||
 
|
||||
|
||||
### Financial
|
||||
|
||||
- [微晓朵](https://www.weixiaoduo.com/)
|
||||
<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)
|
||||
- [WAF PRO](https://waf.pro/)
|
||||
- [盾云SCDN](https://scdn.ddunyun.com/)
|
||||
<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>
|
||||
|
||||
### Partners
|
||||
|
||||
<a href="https://1ms.run">
|
||||
<img height="80" src=".github/assets/1ms.svg" alt="毫秒镜像提供经过审核的 Docker 镜像加速服务">
|
||||
</a>
|
||||
|
||||
### Sponsors
|
||||
|
||||
<p align="center">
|
||||
<a target="_blank" href="https://afdian.com/a/TheTNB">
|
||||
@@ -129,18 +154,23 @@ If the Rat Panel is helpful to you, welcome to [sponsor us](https://opencollecti
|
||||
|
||||
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/TheTNB/panel/graphs/contributors">
|
||||
<img alt="contributors" src="https://contrib.rocks/image?repo=TheTNB/panel"/>
|
||||
</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/#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>
|
||||
<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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# 警告:未经指导私自修改此配置文件可能导致严重后果
|
||||
# Warning: Modifying this configuration file without guidance may cause serious consequences
|
||||
app:
|
||||
debug: false
|
||||
key: a-long-string-with-32-characters
|
||||
|
||||
@@ -3,133 +3,96 @@ module github.com/TheTNB/panel
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/bddjr/hlfhr v1.2.6
|
||||
github.com/beevik/ntp v1.4.3
|
||||
github.com/docker/docker v27.3.1+incompatible
|
||||
github.com/docker/go-connections v0.5.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/expr-lang/expr v1.16.9
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.3
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.22.1
|
||||
github.com/go-playground/validator/v10 v10.23.0
|
||||
github.com/go-rat/chix v1.1.4
|
||||
github.com/go-rat/gormstore v1.0.6
|
||||
github.com/go-rat/sessions v1.0.11
|
||||
github.com/go-rat/utils v1.0.3
|
||||
github.com/go-resty/resty/v2 v2.15.3
|
||||
github.com/go-rat/utils v1.0.7
|
||||
github.com/go-resty/resty/v2 v2.16.2
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/goccy/go-yaml v1.12.0
|
||||
github.com/gookit/color v1.5.4
|
||||
github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a
|
||||
github.com/gomodule/redigo v1.9.2
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0
|
||||
github.com/knadh/koanf/providers/file v1.1.2
|
||||
github.com/knadh/koanf/v2 v2.1.1
|
||||
github.com/knadh/koanf/v2 v2.1.2
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/libdns/alidns v1.0.3
|
||||
github.com/libdns/cloudflare v0.1.1
|
||||
github.com/libdns/dnspod v0.0.3
|
||||
github.com/libdns/huaweicloud v0.3.1
|
||||
github.com/libdns/libdns v0.2.2
|
||||
github.com/libdns/tencentcloud v1.0.0
|
||||
github.com/libdns/tencentcloud v1.1.0
|
||||
github.com/mholt/acmez/v2 v2.0.3
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.8
|
||||
github.com/orandin/slog-gorm v1.4.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samber/do/v2 v2.0.0-beta.7
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/sethvargo/go-limiter v1.0.0
|
||||
github.com/shirou/gopsutil v2.21.11+incompatible
|
||||
github.com/spf13/cast v1.7.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241115180907-128af6df1765
|
||||
github.com/urfave/cli/v3 v3.0.0-beta1
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/net v0.30.0
|
||||
go.uber.org/zap/exp v0.3.0
|
||||
golang.org/x/crypto v0.29.0
|
||||
golang.org/x/net v0.31.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/gorm v1.25.12
|
||||
moul.io/zapgorm2 v1.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.4.14 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/bodgit/plumbing v1.2.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.3.0 // indirect
|
||||
github.com/bodgit/windows v1.0.0 // indirect
|
||||
github.com/connesc/cipherio v0.2.1 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.10.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-rat/securecookie v1.0.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/gofiber/schema v1.2.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jaevor/go-nanoid v1.4.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.1 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/term v0.5.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.15 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/samber/go-type-to-string v1.4.0 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1051 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/ulikunitz/xz v0.5.12 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
|
||||
go.opentelemetry.io/otel v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.30.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.60.1 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.32.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/mholt/acmez/v2 => github.com/TheTNB/acmez/v2 v2.0.0-20241012163130-5833d84639e0
|
||||
replace (
|
||||
github.com/libdns/tencentcloud => github.com/devhaozi/tencentcloud v0.0.0-20241129095955-7d4e898ce504
|
||||
github.com/mholt/acmez/v2 => github.com/TheTNB/acmez/v2 v2.0.0-20241025203320-cc718c4c870b
|
||||
)
|
||||
|
||||
@@ -1,97 +1,35 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/TheTNB/acmez/v2 v2.0.0-20241012163130-5833d84639e0 h1:o66ZhpjXVs7oi3VyiZvlpp9StFFe4KTeklzB/T4s59Q=
|
||||
github.com/TheTNB/acmez/v2 v2.0.0-20241012163130-5833d84639e0/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/TheTNB/acmez/v2 v2.0.0-20241025203320-cc718c4c870b h1:1iVxCglpEJ/kq48YcujkWq8SeOacMuMmuekMmGyOSPA=
|
||||
github.com/TheTNB/acmez/v2 v2.0.0-20241025203320-cc718c4c870b/go.mod h1:pQ1ysaDeGrIMvJ9dfJMk5kJNkn7L2sb3UhyrX6Q91cw=
|
||||
github.com/bddjr/hlfhr v1.2.6 h1:t1nu0JH535WfxUOceyGzU3UHmv29s+p6V47UdyYMv9A=
|
||||
github.com/bddjr/hlfhr v1.2.6/go.mod h1:oyIv4Q9JpCgZFdtH3KyTNWp7YYRWl4zl8k4ozrMAB4g=
|
||||
github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho=
|
||||
github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM=
|
||||
github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8=
|
||||
github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY=
|
||||
github.com/bodgit/sevenzip v1.3.0/go.mod h1:omwNcgZTEooWM8gA/IJ2Nk/+ZQ94+GsytRzOJJ8FBlM=
|
||||
github.com/bodgit/windows v1.0.0 h1:rLQ/XjsleZvx4fR1tB/UxQrK+SJ2OFHzfPjLWWOhDIA=
|
||||
github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw=
|
||||
github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
|
||||
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY=
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/devhaozi/tencentcloud v0.0.0-20241129095955-7d4e898ce504 h1:Wk7RQyamGDfAz+5hZIT/GIm6ay5cORztgYVXOJTobKY=
|
||||
github.com/devhaozi/tencentcloud v0.0.0-20241129095955-7d4e898ce504/go.mod h1:o0+WCxQ7LGLtyjnjYU4HbGW9uVjN44SdUDhxdUYLGPw=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
|
||||
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
|
||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.3 h1:ei3Vq/rpPI/jCJY9mRHJAKg5vU+EhZyWhBAkaAomQuw=
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.3/go.mod h1:VJ9FIOBAur+NmQ8c4tDVwOuiJcgupTG105FexPFrXzA=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
@@ -100,8 +38,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-rat/chix v1.1.4 h1:YVDwF4vrlkm5hmlDGWKoiWPgP98pJooBvhsto8UWq+o=
|
||||
github.com/go-rat/chix v1.1.4/go.mod h1:eKUTG7GXqMS278PH0QZi8yMWZddvMfxqWu7mverM6kQ=
|
||||
github.com/go-rat/gormstore v1.0.6 h1:YxmaY6n3JoTk6o9RXoZ1+cQ6HQL+ayf/6d8D76ydCAc=
|
||||
@@ -110,100 +48,46 @@ github.com/go-rat/securecookie v1.0.1 h1:HW0fpKmB+FjJzXTw8ABOwBJ+XrPmRBSZqHhmrv8
|
||||
github.com/go-rat/securecookie v1.0.1/go.mod h1:tP/ObWYyjmcpabQ7WTon/i2lBSip/Aolliw2llXuPDU=
|
||||
github.com/go-rat/sessions v1.0.11 h1:2eTUvhEBtymrAtwv71LlGHNylz+Ad2SgY3jg/sSUboA=
|
||||
github.com/go-rat/sessions v1.0.11/go.mod h1:Ray/GCbuhm4U9xpjFFSCfOTCEn91puEhAXX5creHE9g=
|
||||
github.com/go-rat/utils v1.0.3 h1:SqH/O0KYq4SBv8naSAjJA4hC45/d8NLNrTCONfqjbFM=
|
||||
github.com/go-rat/utils v1.0.3/go.mod h1:4WNPlrF57KmeGZN3HOeBgdBVLJL3xgba4QRP/t6pKrE=
|
||||
github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8=
|
||||
github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-rat/utils v1.0.7 h1:hZUe+n1W/tvF3j+oP/TeAHX3AF98SjVB1FuF11zkCAI=
|
||||
github.com/go-rat/utils v1.0.7/go.mod h1:b0S/vd2bXWvPUQpw8z6RmJrUG7G7BkMuNSrsz6JRBHs=
|
||||
github.com/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
|
||||
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
|
||||
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg=
|
||||
github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a h1:BAyyIK6rc6Tq9vT2KVlPJXD8EVv/LpaFwn4fgfLwtHg=
|
||||
github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a/go.mod h1:bgk4Ij/0OQ89UeoFFAQrSNhbbr4rKJ0fwWfo7wc+TCc=
|
||||
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
|
||||
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jaevor/go-nanoid v1.4.0 h1:mPz0oi3CrQyEtRxeRq927HHtZCJAAtZ7zdy7vOkrvWs=
|
||||
github.com/jaevor/go-nanoid v1.4.0/go.mod h1:GIpPtsvl3eSBsjjIEFQdzzgpi50+Bo1Luk+aYlbJzlc=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
|
||||
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
|
||||
github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w=
|
||||
github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
|
||||
github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM=
|
||||
github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ=
|
||||
github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
@@ -218,340 +102,98 @@ github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ=
|
||||
github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE=
|
||||
github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
|
||||
github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
|
||||
github.com/libdns/dnspod v0.0.3 h1:xJHDIujgLjvZnpB8/rMoCHUqA/KxSGBqRUXxSIzNzAA=
|
||||
github.com/libdns/dnspod v0.0.3/go.mod h1:XLnqMmK7QlLPEbHwcOxbRvlzRvDgaaUlthRNFOPjXPI=
|
||||
github.com/libdns/huaweicloud v0.3.1 h1:wro0zpG86JKL3QVEaV/xfHW/59rmqwMYhcbl16w9EYg=
|
||||
github.com/libdns/huaweicloud v0.3.1/go.mod h1:6s5ZIwLUr2qKsQz2SRNo7uh8X5uJohxoBR95rtSpNcI=
|
||||
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/libdns/tencentcloud v1.0.0 h1:u4LXnYu/lu/9P5W+MCVPeSDnwI+6w+DxYhQ1wSnQOuU=
|
||||
github.com/libdns/tencentcloud v1.0.0/go.mod h1:NlCgPumzUsZWSOo1+Q/Hfh8G6TNRAaTUeWQdg6LbtUI=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM=
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.8/go.mod h1:5f7FUYGXdJWUjESffJaYR4R60VhnHxb2X3T1teMyv5A=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U=
|
||||
github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
|
||||
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
|
||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU=
|
||||
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/samber/do/v2 v2.0.0-beta.7 h1:tmdLOVSCbTA6uGWLU5poi/nZvMRh5QxXFJ9vHytU+Jk=
|
||||
github.com/samber/do/v2 v2.0.0-beta.7/go.mod h1:+LpV3vu4L81Q1JMZNSkMvSkW9lt4e5eJoXoZHkeBS4c=
|
||||
github.com/samber/go-type-to-string v1.4.0 h1:KXphToZgiFdnJQxryU25brhlh/CqY/cwJVeX2rfmow0=
|
||||
github.com/samber/go-type-to-string v1.4.0/go.mod h1:jpU77vIDoIxkahknKDoEx9C8bQ1ADnh2sotZ8I4QqBU=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4=
|
||||
github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II=
|
||||
github.com/shirou/gopsutil v2.21.11+incompatible h1:lOGOyCG67a5dv2hq5Z1BLDUqqKp3HkbjPcz5j6XMS0U=
|
||||
github.com/shirou/gopsutil v2.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597 h1:C0GHdLTfikLVoEzfhgPfrZ7LwlG0xiCmk6iwNKE+xs0=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.597/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
|
||||
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
|
||||
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1051 h1:3mg0L9vv9eO8UN4Oa7vNawe6yUIuXf9D0Q79rUmnblo=
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1051/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8 h1:7yw1yHkylDcu/CwY4hEEe8MycDLo7B9LjcqwhoL8NrM=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241013191809-e73b7dd454e8/go.mod h1:itu4KWRgrfEwGcfNka+rV4houuirUau53i0diN4lG5g=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1 h1:1fJU+bltkwN8lF4Sni/X0i1d8XwPIrS82ivZ8qsp/q4=
|
||||
github.com/urfave/cli/v3 v3.0.0-alpha9.1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241115180907-128af6df1765 h1:nnw6RpVhgrUvV6BLysdedhxaALJY1JqRFr3S85XXpF4=
|
||||
github.com/tufanbarisyildirim/gonginx v0.0.0-20241115180907-128af6df1765/go.mod h1:itu4KWRgrfEwGcfNka+rV4houuirUau53i0diN4lG5g=
|
||||
github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg=
|
||||
github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
|
||||
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
|
||||
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8=
|
||||
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
|
||||
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
|
||||
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
|
||||
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
|
||||
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go4.org v0.0.0-20200411211856-f5505b9728dd h1:BNJlw5kRTzdmyfh5U8F93HA2OwkP7ZGwA51eJ/0wKOU=
|
||||
go4.org v0.0.0-20200411211856-f5505b9728dd/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
|
||||
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
|
||||
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
|
||||
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4=
|
||||
@@ -576,8 +218,3 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
moul.io/zapgorm2 v1.3.0 h1:+CzUTMIcnafd0d/BvBce8T4uPn6DQnpIrz64cyixlkk=
|
||||
moul.io/zapgorm2 v1.3.0/go.mod h1:nPVy6U9goFKHR4s+zfSo1xVFaoU7Qgd5DoCdOfzoCqs=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
ut "github.com/go-playground/universal-translator"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/go-rat/sessions"
|
||||
"github.com/go-rat/utils/crypt"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/robfig/cron/v3"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/queue"
|
||||
@@ -22,12 +24,35 @@ var (
|
||||
Session *sessions.Manager
|
||||
Cron *cron.Cron
|
||||
Queue *queue.Queue
|
||||
Logger *zap.Logger
|
||||
Logger *slog.Logger
|
||||
Crypter crypt.Crypter
|
||||
)
|
||||
|
||||
// 面板状态常量
|
||||
const (
|
||||
StatusNormal = iota
|
||||
StatusMaintain
|
||||
StatusClosed
|
||||
StatusUpgrade
|
||||
StatusFailed
|
||||
)
|
||||
|
||||
// 面板全局变量
|
||||
var (
|
||||
Root string
|
||||
Version string
|
||||
Locale string
|
||||
Key string
|
||||
Root string
|
||||
Locale string
|
||||
IsCli bool
|
||||
Status = StatusNormal
|
||||
)
|
||||
|
||||
// 自动注入
|
||||
var (
|
||||
Version = "0.0.0"
|
||||
BuildTime string
|
||||
CommitHash string
|
||||
GoVersion string
|
||||
BuildID string
|
||||
BuildUser string
|
||||
BuildHost string
|
||||
)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package benchmark
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "benchmark",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Post("/test", service.Test)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package benchmark
|
||||
|
||||
type Test struct {
|
||||
Name string `json:"name" validate:"required,oneof=image machine compile encryption compression physics json memory disk"`
|
||||
Multi bool `json:"multi"`
|
||||
}
|
||||
@@ -0,0 +1,685 @@
|
||||
package benchmark
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
cryptorand "crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
// Test 运行测试
|
||||
func (s *Service) Test(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Test](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Name {
|
||||
case "image":
|
||||
result := s.imageProcessing(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "machine":
|
||||
result := s.machineLearning(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "compile":
|
||||
result := s.compileSimulationSingle(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "encryption":
|
||||
result := s.encryptionTest(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "compression":
|
||||
result := s.compressionTest(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "physics":
|
||||
result := s.physicsSimulation(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "json":
|
||||
result := s.jsonProcessing(req.Multi)
|
||||
service.Success(w, result)
|
||||
case "disk":
|
||||
result := s.diskTestTask()
|
||||
service.Success(w, result)
|
||||
case "memory":
|
||||
result := s.memoryTestTask()
|
||||
service.Success(w, result)
|
||||
default:
|
||||
service.Error(w, http.StatusUnprocessableEntity, "未知测试类型")
|
||||
}
|
||||
}
|
||||
|
||||
// calculateCpuScore 计算CPU成绩
|
||||
func (s *Service) calculateCpuScore(duration time.Duration) int {
|
||||
score := int((10 / duration.Seconds()) * float64(3000))
|
||||
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// calculateScore 计算内存/硬盘成绩
|
||||
func (s *Service) calculateScore(duration time.Duration) int {
|
||||
score := int((20 / duration.Seconds()) * float64(30000))
|
||||
|
||||
if score < 0 {
|
||||
score = 0
|
||||
}
|
||||
return score
|
||||
}
|
||||
|
||||
// 图像处理
|
||||
|
||||
func (s *Service) imageProcessing(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
if err := s.imageProcessingTask(n); err != nil {
|
||||
return 0
|
||||
}
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) imageProcessingTask(numThreads int) error {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 4000, 4000))
|
||||
for x := 0; x < 4000; x++ {
|
||||
for y := 0; y < 4000; y++ {
|
||||
img.Set(x, y, color.RGBA{R: uint8(x % 256), G: uint8(y % 256), A: 255})
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
dx := img.Bounds().Dx()
|
||||
dy := img.Bounds().Dy()
|
||||
chunkSize := dy / numThreads
|
||||
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
startY := i * chunkSize
|
||||
endY := startY + chunkSize
|
||||
if i == numThreads-1 {
|
||||
endY = dy
|
||||
}
|
||||
for x := 1; x < dx-1; x++ {
|
||||
for y := startY + 1; y < endY-1; y++ {
|
||||
// 卷积操作(模糊)
|
||||
rTotal, gTotal, bTotal := 0, 0, 0
|
||||
for k := -1; k <= 1; k++ {
|
||||
for l := -1; l <= 1; l++ {
|
||||
r, g, b, _ := img.At(x+k, y+l).RGBA()
|
||||
rTotal += int(r)
|
||||
gTotal += int(g)
|
||||
bTotal += int(b)
|
||||
}
|
||||
}
|
||||
rAvg := uint8(rTotal / 9 / 256)
|
||||
gAvg := uint8(gTotal / 9 / 256)
|
||||
bAvg := uint8(bTotal / 9 / 256)
|
||||
img.Set(x, y, color.RGBA{R: rAvg, G: gAvg, B: bAvg, A: 255})
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 机器学习(矩阵乘法)
|
||||
|
||||
func (s *Service) machineLearning(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.machineLearningTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) machineLearningTask(numThreads int) {
|
||||
size := 900
|
||||
a := make([][]float64, size)
|
||||
b := make([][]float64, size)
|
||||
for i := 0; i < size; i++ {
|
||||
a[i] = make([]float64, size)
|
||||
b[i] = make([]float64, size)
|
||||
for j := 0; j < size; j++ {
|
||||
a[i][j] = rand.Float64()
|
||||
b[i][j] = rand.Float64()
|
||||
}
|
||||
}
|
||||
|
||||
c := make([][]float64, size)
|
||||
for i := 0; i < size; i++ {
|
||||
c[i] = make([]float64, size)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
chunkSize := size / numThreads
|
||||
|
||||
for k := 0; k < numThreads; k++ {
|
||||
wg.Add(1)
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
start := k * chunkSize
|
||||
end := start + chunkSize
|
||||
if k == numThreads-1 {
|
||||
end = size
|
||||
}
|
||||
for i := start; i < end; i++ {
|
||||
for j := 0; j < size; j++ {
|
||||
sum := 0.0
|
||||
for l := 0; l < size; l++ {
|
||||
sum += a[i][l] * b[l][j]
|
||||
}
|
||||
c[i][j] = sum
|
||||
}
|
||||
}
|
||||
}(k)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 数学问题(计算斐波那契数)
|
||||
|
||||
func (s *Service) compileSimulationSingle(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
totalCalculations := 1000
|
||||
fibNumber := 20000
|
||||
|
||||
calculationsPerThread := totalCalculations / n
|
||||
remainder := totalCalculations % n
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
tasks := calculationsPerThread
|
||||
if i < remainder {
|
||||
tasks++ // 处理无法均分的剩余任务
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(tasks int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < tasks; j++ {
|
||||
s.fib(fibNumber)
|
||||
}
|
||||
}(tasks)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
// 斐波那契函数
|
||||
func (s *Service) fib(n int) *big.Int {
|
||||
if n < 2 {
|
||||
return big.NewInt(int64(n))
|
||||
}
|
||||
a := big.NewInt(0)
|
||||
b := big.NewInt(1)
|
||||
temp := big.NewInt(0)
|
||||
for i := 2; i <= n; i++ {
|
||||
temp.Add(a, b)
|
||||
a.Set(b)
|
||||
b.Set(temp)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// AES加密
|
||||
|
||||
func (s *Service) encryptionTest(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
if err := s.encryptionTestTask(n); err != nil {
|
||||
return 0
|
||||
}
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) encryptionTestTask(numThreads int) error {
|
||||
key := []byte("abcdefghijklmnopqrstuvwxyz123456")
|
||||
dataSize := 1 * 1024 * 1024 * 1024 // 1GB
|
||||
plaintext := []byte(strings.Repeat("A", dataSize))
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
aesGCM, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chunkSize := dataSize / numThreads
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
start := i * chunkSize
|
||||
end := start + chunkSize
|
||||
if i == numThreads-1 {
|
||||
end = dataSize
|
||||
}
|
||||
|
||||
nonce := make([]byte, aesGCM.NonceSize())
|
||||
if _, err = cryptorand.Read(nonce); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
aesGCM.Seal(nil, nonce, plaintext[start:end], nil)
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 压缩/解压缩
|
||||
|
||||
func (s *Service) compressionTest(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.compressionTestTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) compressionTestTask(numThreads int) {
|
||||
data := []byte(strings.Repeat("耗子面板", 50000000))
|
||||
chunkSize := len(data) / numThreads
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
compressedChunks := make([]bytes.Buffer, numThreads)
|
||||
|
||||
// 压缩
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
start := i * chunkSize
|
||||
end := start + chunkSize
|
||||
if i == numThreads-1 {
|
||||
end = len(data)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
w := gzip.NewWriter(&buf)
|
||||
_, _ = w.Write(data[start:end])
|
||||
_ = w.Close()
|
||||
compressedChunks[i] = buf
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 解压缩
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
r, err := gzip.NewReader(&compressedChunks[i])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_, err = io.Copy(io.Discard, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = r.Close()
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 物理仿真(N体问题)
|
||||
|
||||
func (s *Service) physicsSimulation(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.physicsSimulationTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) physicsSimulationTask(numThreads int) {
|
||||
const (
|
||||
numBodies = 4000
|
||||
steps = 30
|
||||
)
|
||||
|
||||
type Body struct {
|
||||
x, y, z, vx, vy, vz float64
|
||||
}
|
||||
|
||||
bodies := make([]Body, numBodies)
|
||||
for i := 0; i < numBodies; i++ {
|
||||
bodies[i] = Body{
|
||||
x: rand.Float64(),
|
||||
y: rand.Float64(),
|
||||
z: rand.Float64(),
|
||||
vx: rand.Float64(),
|
||||
vy: rand.Float64(),
|
||||
vz: rand.Float64(),
|
||||
}
|
||||
}
|
||||
|
||||
chunkSize := numBodies / numThreads
|
||||
|
||||
for step := 0; step < steps; step++ {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 更新速度
|
||||
for k := 0; k < numThreads; k++ {
|
||||
wg.Add(1)
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
start := k * chunkSize
|
||||
end := start + chunkSize
|
||||
if k == numThreads-1 {
|
||||
end = numBodies
|
||||
}
|
||||
for i := start; i < end; i++ {
|
||||
bi := &bodies[i]
|
||||
for j := 0; j < numBodies; j++ {
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
bj := &bodies[j]
|
||||
dx := bj.x - bi.x
|
||||
dy := bj.y - bi.y
|
||||
dz := bj.z - bi.z
|
||||
dist := math.Sqrt(dx*dx + dy*dy + dz*dz)
|
||||
if dist == 0 {
|
||||
continue
|
||||
}
|
||||
force := 1 / (dist * dist)
|
||||
bi.vx += force * dx / dist
|
||||
bi.vy += force * dy / dist
|
||||
bi.vz += force * dz / dist
|
||||
}
|
||||
}
|
||||
}(k)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// 更新位置
|
||||
for k := 0; k < numThreads; k++ {
|
||||
wg.Add(1)
|
||||
go func(k int) {
|
||||
defer wg.Done()
|
||||
start := k * chunkSize
|
||||
end := start + chunkSize
|
||||
if k == numThreads-1 {
|
||||
end = numBodies
|
||||
}
|
||||
for i := start; i < end; i++ {
|
||||
bi := &bodies[i]
|
||||
bi.x += bi.vx
|
||||
bi.y += bi.vy
|
||||
bi.z += bi.vz
|
||||
}
|
||||
}(k)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// JSON解析
|
||||
|
||||
func (s *Service) jsonProcessing(multi bool) int {
|
||||
n := 1
|
||||
if multi {
|
||||
n = runtime.NumCPU()
|
||||
}
|
||||
start := time.Now()
|
||||
s.jsonProcessingTask(n)
|
||||
duration := time.Since(start)
|
||||
return s.calculateCpuScore(duration)
|
||||
}
|
||||
|
||||
func (s *Service) jsonProcessingTask(numThreads int) {
|
||||
numElements := 1000000
|
||||
elementsPerThread := numElements / numThreads
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < numThreads; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
start := i * elementsPerThread
|
||||
end := start + elementsPerThread
|
||||
if i == numThreads-1 {
|
||||
end = numElements
|
||||
}
|
||||
|
||||
elements := make([]map[string]any, 0, end-start)
|
||||
for j := start; j < end; j++ {
|
||||
elements = append(elements, map[string]any{
|
||||
"id": j,
|
||||
"value": fmt.Sprintf("Value%d", j),
|
||||
})
|
||||
}
|
||||
encoded, err := json.Marshal(elements)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var parsed []map[string]any
|
||||
err = json.Unmarshal(encoded, &parsed)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 内存性能
|
||||
|
||||
func (s *Service) memoryTestTask() map[string]any {
|
||||
results := make(map[string]any)
|
||||
dataSize := 500 * 1024 * 1024 // 500 MB
|
||||
data := make([]byte, dataSize)
|
||||
_, _ = cryptorand.Read(data)
|
||||
|
||||
start := time.Now()
|
||||
// 内存读写速度
|
||||
results["bandwidth"] = s.memoryBandwidthTest(data)
|
||||
// 内存访问延迟
|
||||
data = data[:100*1024*1024] // 100 MB
|
||||
results["latency"] = s.memoryLatencyTest(data)
|
||||
duration := time.Since(start)
|
||||
results["score"] = s.calculateScore(duration)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (s *Service) memoryBandwidthTest(data []byte) string {
|
||||
dataSize := len(data)
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
for i := 0; i < dataSize; i++ {
|
||||
data[i] ^= 0xFF
|
||||
}
|
||||
|
||||
duration := time.Since(startTime).Seconds()
|
||||
if duration == 0 {
|
||||
return "N/A"
|
||||
}
|
||||
speed := float64(dataSize) / duration / (1024 * 1024)
|
||||
return fmt.Sprintf("%.2f MB/s", speed)
|
||||
}
|
||||
|
||||
func (s *Service) memoryLatencyTest(data []byte) string {
|
||||
dataSize := len(data)
|
||||
indices := rand.Perm(dataSize)
|
||||
|
||||
startTime := time.Now()
|
||||
sum := byte(0)
|
||||
for _, idx := range indices {
|
||||
sum ^= data[idx]
|
||||
}
|
||||
duration := time.Since(startTime).Seconds()
|
||||
if duration == 0 {
|
||||
return "N/A"
|
||||
}
|
||||
avgLatency := duration * 1e9 / float64(dataSize)
|
||||
return fmt.Sprintf("%.2f ns", avgLatency)
|
||||
}
|
||||
|
||||
// 硬盘IO
|
||||
|
||||
func (s *Service) diskTestTask() map[string]any {
|
||||
results := make(map[string]any)
|
||||
blockSizes := []int64{4 * 1024, 64 * 1024, 512 * 1024, 1 * 1024 * 1024} // 4K, 64K, 512K, 1M
|
||||
fileSize := int64(100 * 1024 * 1024) // 100MB 文件
|
||||
|
||||
start := time.Now()
|
||||
for _, blockSize := range blockSizes {
|
||||
result := s.diskIOTest(blockSize, fileSize)
|
||||
results[fmt.Sprintf("%d", blockSize/1024)] = result
|
||||
}
|
||||
duration := time.Since(start)
|
||||
results["score"] = s.calculateScore(duration)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func (s *Service) diskIOTest(blockSize int64, fileSize int64) map[string]any {
|
||||
result := make(map[string]any)
|
||||
tempFile := fmt.Sprintf("tempfile_%d", blockSize)
|
||||
defer os.Remove(tempFile)
|
||||
|
||||
// 写测试
|
||||
writeSpeed, writeIOPS := s.diskWriteTest(tempFile, blockSize, fileSize)
|
||||
// 读测试
|
||||
readSpeed, readIOPS := s.diskReadTest(tempFile, blockSize, fileSize)
|
||||
|
||||
result["write_speed"] = fmt.Sprintf("%.2f MB/s", writeSpeed)
|
||||
result["write_iops"] = fmt.Sprintf("%.2f IOPS", writeIOPS)
|
||||
result["read_speed"] = fmt.Sprintf("%.2f MB/s", readSpeed)
|
||||
result["read_iops"] = fmt.Sprintf("%.2f IOPS", readIOPS)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Service) diskWriteTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
|
||||
totalBlocks := fileSize / blockSize
|
||||
|
||||
data := make([]byte, blockSize)
|
||||
_, _ = cryptorand.Read(data)
|
||||
|
||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_SYNC, 0644)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
for i := int64(0); i < totalBlocks; i++ {
|
||||
// 生成随机偏移
|
||||
offset := rand.Int64N(fileSize - blockSize + 1)
|
||||
_, err := file.WriteAt(data, offset)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
_ = file.Sync()
|
||||
|
||||
duration := time.Since(start).Seconds()
|
||||
if duration == 0 {
|
||||
duration = 1
|
||||
}
|
||||
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
|
||||
iops := float64(totalBlocks) / duration
|
||||
return speed, iops
|
||||
}
|
||||
|
||||
func (s *Service) diskReadTest(fileName string, blockSize int64, fileSize int64) (float64, float64) {
|
||||
totalBlocks := fileSize / blockSize
|
||||
|
||||
data := make([]byte, blockSize)
|
||||
|
||||
file, err := os.OpenFile(fileName, os.O_RDONLY|os.O_SYNC, 0644)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
start := time.Now()
|
||||
|
||||
for i := int64(0); i < totalBlocks; i++ {
|
||||
// 生成随机偏移
|
||||
offset := rand.Int64N(fileSize - blockSize + 1)
|
||||
_, err := file.ReadAt(data, offset)
|
||||
if err != nil && err != io.EOF {
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
duration := time.Since(start).Seconds()
|
||||
if duration == 0 {
|
||||
duration = 1
|
||||
}
|
||||
speed := float64(totalBlocks*blockSize) / duration / (1024 * 1024)
|
||||
iops := float64(totalBlocks) / duration
|
||||
return speed, iops
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "docker",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package docker
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/docker/daemon.json")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/docker/daemon.json", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("docker"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/go-rat/utils/str"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
@@ -14,9 +15,7 @@ import (
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -55,7 +54,6 @@ func (s *Service) List(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
@@ -63,7 +61,6 @@ func (s *Service) List(w http.ResponseWriter, r *http.Request) {
|
||||
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]),
|
||||
@@ -128,7 +125,6 @@ port = ` + ports + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
action = %(action_mwl)s
|
||||
logpath = ` + app.Root + `/wwwlogs/` + website.Name + `.log
|
||||
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-END
|
||||
`
|
||||
@@ -158,25 +154,17 @@ ignoreregex =
|
||||
}
|
||||
|
||||
case "service":
|
||||
var logPath string
|
||||
var filter string
|
||||
var port string
|
||||
var err error
|
||||
switch jailName {
|
||||
case "ssh":
|
||||
if os.IsDebian() || os.IsUbuntu() {
|
||||
logPath = "/var/log/auth.log"
|
||||
} else {
|
||||
logPath = "/var/log/secure"
|
||||
}
|
||||
filter = "sshd"
|
||||
port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}'")
|
||||
case "mysql":
|
||||
logPath = app.Root + "/server/mysql/mysql-error.log"
|
||||
filter = "mysqld-auth"
|
||||
port, err = shell.Execf("cat %s/server/mysql/conf/my.cnf | grep 'port' | head -n 1 | awk '{print $3}'", app.Root)
|
||||
case "pure-ftpd":
|
||||
logPath = "/var/log/messages"
|
||||
filter = "pure-ftpd"
|
||||
port, err = shell.Execf(`cat %s/server/pure-ftpd/etc/pure-ftpd.conf | grep "Bind" | awk '{print $2}' | awk -F "," '{print $2}'`, app.Root)
|
||||
default:
|
||||
@@ -197,8 +185,6 @@ port = ` + port + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
action = %(action_mwl)s
|
||||
logpath = ` + logPath + `
|
||||
# ` + jailName + `-END
|
||||
`
|
||||
raw += rule
|
||||
@@ -208,7 +194,7 @@ logpath = ` + logPath + `
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
|
||||
if _, err = shell.Execf("fail2ban-client reload"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "重载配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package fail2ban
|
||||
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"`
|
||||
|
||||
@@ -3,9 +3,12 @@ package apps
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
_ "github.com/TheTNB/panel/internal/apps/benchmark"
|
||||
_ "github.com/TheTNB/panel/internal/apps/docker"
|
||||
_ "github.com/TheTNB/panel/internal/apps/fail2ban"
|
||||
_ "github.com/TheTNB/panel/internal/apps/frp"
|
||||
_ "github.com/TheTNB/panel/internal/apps/gitea"
|
||||
_ "github.com/TheTNB/panel/internal/apps/memcached"
|
||||
_ "github.com/TheTNB/panel/internal/apps/mysql"
|
||||
_ "github.com/TheTNB/panel/internal/apps/nginx"
|
||||
_ "github.com/TheTNB/panel/internal/apps/php"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package memcached
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "memcached",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package memcached
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package memcached
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type Service struct{}
|
||||
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("memcached")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "failed to get Memcached status: %v", err)
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", "127.0.0.1:11211")
|
||||
if err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte("stats\nquit\n"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "failed to write to Memcached: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
data := make([]types.NV, 0)
|
||||
re := regexp.MustCompile(`STAT\s(\S+)\s(\S+)`)
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if matches := re.FindStringSubmatch(line); len(matches) == 3 {
|
||||
data = append(data, types.NV{
|
||||
Name: matches[1],
|
||||
Value: matches[2],
|
||||
})
|
||||
}
|
||||
if line == "END" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "failed to read from Memcached: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/systemd/system/memcached.service")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/systemd/system/memcached.service", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("memcached"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -15,7 +15,6 @@ func init() {
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/errorLog", service.ErrorLog)
|
||||
r.Post("/clearErrorLog", service.ClearErrorLog)
|
||||
r.Get("/slowLog", service.SlowLog)
|
||||
r.Post("/clearSlowLog", service.ClearSlowLog)
|
||||
|
||||
@@ -5,5 +5,5 @@ type UpdateConfig struct {
|
||||
}
|
||||
|
||||
type SetRootPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
@@ -123,7 +123,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
if len(matches) > 1 {
|
||||
d := map[string]string{"name": expression.name, "value": matches[1]}
|
||||
if expression.name == "发送" || expression.name == "接收" {
|
||||
d["value"] = str.FormatBytes(cast.ToFloat64(matches[1]))
|
||||
d["value"] = tools.FormatBytes(cast.ToFloat64(matches[1]))
|
||||
}
|
||||
|
||||
load = append(load, d)
|
||||
@@ -142,15 +142,9 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, load)
|
||||
}
|
||||
|
||||
// ErrorLog 获取错误日志
|
||||
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
|
||||
log, _ := shell.Execf("tail -n 100 %s/server/mysql/mysql-error.log", app.Root)
|
||||
service.Success(w, log)
|
||||
}
|
||||
|
||||
// ClearErrorLog 清空错误日志
|
||||
func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf("echo '' > %s/server/mysql/mysql-error.log", app.Root); err != nil {
|
||||
if err := systemctl.LogsClear("mysqld"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -160,8 +154,7 @@ func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (s *Service) SlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
log, _ := shell.Execf("tail -n 100 %s/server/mysql/mysql-slow.log", app.Root)
|
||||
service.Success(w, log)
|
||||
service.Success(w, fmt.Sprintf("%s/server/mysql/mysql-slow.log", app.Root))
|
||||
}
|
||||
|
||||
// ClearSlowLog 清空慢查询日志
|
||||
@@ -206,7 +199,7 @@ func (s *Service) SetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err = mysql.UserPassword("root", req.Password); err != nil {
|
||||
if err = mysql.UserPassword("root", req.Password, "localhost"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
@@ -58,8 +58,7 @@ func (s *Service) SaveConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
|
||||
out, _ := shell.Execf("tail -n 100 %s/%s", app.Root, "wwwlogs/nginx-error.log")
|
||||
service.Success(w, out)
|
||||
service.Success(w, fmt.Sprintf("%s/%s", app.Root, "wwwlogs/nginx-error.log"))
|
||||
}
|
||||
|
||||
func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -96,7 +95,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusInternalServerError, "获取负载失败")
|
||||
return
|
||||
}
|
||||
mem := str.FormatBytes(cast.ToFloat64(out))
|
||||
mem := tools.FormatBytes(cast.ToFloat64(out))
|
||||
data = append(data, types.NV{
|
||||
Name: "内存占用",
|
||||
Value: mem,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package php
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
@@ -8,76 +10,26 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "php80",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService(80)
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/fpmConfig", service.GetFPMConfig)
|
||||
r.Post("/fpmConfig", service.UpdateFPMConfig)
|
||||
r.Get("/errorLog", service.ErrorLog)
|
||||
r.Get("/slowLog", service.SlowLog)
|
||||
r.Post("/clearErrorLog", service.ClearErrorLog)
|
||||
r.Post("/clearSlowLog", service.ClearSlowLog)
|
||||
r.Get("/extensions", service.ExtensionList)
|
||||
r.Post("/extensions", service.InstallExtension)
|
||||
r.Delete("/extensions", service.UninstallExtension)
|
||||
},
|
||||
})
|
||||
apploader.Register(&types.App{
|
||||
Slug: "php81",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService(81)
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/fpmConfig", service.GetFPMConfig)
|
||||
r.Post("/fpmConfig", service.UpdateFPMConfig)
|
||||
r.Get("/errorLog", service.ErrorLog)
|
||||
r.Get("/slowLog", service.SlowLog)
|
||||
r.Post("/clearErrorLog", service.ClearErrorLog)
|
||||
r.Post("/clearSlowLog", service.ClearSlowLog)
|
||||
r.Get("/extensions", service.ExtensionList)
|
||||
r.Post("/extensions", service.InstallExtension)
|
||||
r.Delete("/extensions", service.UninstallExtension)
|
||||
},
|
||||
})
|
||||
apploader.Register(&types.App{
|
||||
Slug: "php82",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService(82)
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/fpmConfig", service.GetFPMConfig)
|
||||
r.Post("/fpmConfig", service.UpdateFPMConfig)
|
||||
r.Get("/errorLog", service.ErrorLog)
|
||||
r.Get("/slowLog", service.SlowLog)
|
||||
r.Post("/clearErrorLog", service.ClearErrorLog)
|
||||
r.Post("/clearSlowLog", service.ClearSlowLog)
|
||||
r.Get("/extensions", service.ExtensionList)
|
||||
r.Post("/extensions", service.InstallExtension)
|
||||
r.Delete("/extensions", service.UninstallExtension)
|
||||
},
|
||||
})
|
||||
apploader.Register(&types.App{
|
||||
Slug: "php83",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService(83)
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/fpmConfig", service.GetFPMConfig)
|
||||
r.Post("/fpmConfig", service.UpdateFPMConfig)
|
||||
r.Get("/errorLog", service.ErrorLog)
|
||||
r.Get("/slowLog", service.SlowLog)
|
||||
r.Post("/clearErrorLog", service.ClearErrorLog)
|
||||
r.Post("/clearSlowLog", service.ClearSlowLog)
|
||||
r.Get("/extensions", service.ExtensionList)
|
||||
r.Post("/extensions", service.InstallExtension)
|
||||
r.Delete("/extensions", service.UninstallExtension)
|
||||
},
|
||||
})
|
||||
php := []uint{74, 80, 81, 82, 83, 84}
|
||||
for _, version := range php {
|
||||
apploader.Register(&types.App{
|
||||
Slug: fmt.Sprintf("php%d", version),
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService(version)
|
||||
r.Post("/setCli", service.SetCli)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
r.Get("/fpmConfig", service.GetFPMConfig)
|
||||
r.Post("/fpmConfig", service.UpdateFPMConfig)
|
||||
r.Get("/load", service.Load)
|
||||
r.Get("/errorLog", service.ErrorLog)
|
||||
r.Get("/slowLog", service.SlowLog)
|
||||
r.Post("/clearErrorLog", service.ClearErrorLog)
|
||||
r.Post("/clearSlowLog", service.ClearSlowLog)
|
||||
r.Get("/extensions", service.ExtensionList)
|
||||
r.Post("/extensions", service.InstallExtension)
|
||||
r.Delete("/extensions", service.UninstallExtension)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -33,6 +32,15 @@ func NewService(version uint) *Service {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SetCli(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf("ln -sf %s/server/php/%d/bin/php /usr/bin/php", app.Root, s.version); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/php/%d/etc/php.ini", app.Root, s.version))
|
||||
if err != nil {
|
||||
@@ -84,26 +92,25 @@ func (s *Service) UpdateFPMConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
var raw map[string]any
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1/phpfpm_status/%d", s.version))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
service.Error(w, http.StatusInternalServerError, "获取负载状态失败:%v", err)
|
||||
_, err := client.R().SetResult(&raw).Get(fmt.Sprintf("http://127.0.0.1/phpfpm_status/%d?json", s.version))
|
||||
if err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
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"}
|
||||
rawKeys := []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"}
|
||||
|
||||
loads := make([]types.NV, len(dataKeys))
|
||||
loads := make([]types.NV, 0)
|
||||
for i := range dataKeys {
|
||||
loads[i].Name = dataKeys[i]
|
||||
|
||||
r := regexp.MustCompile(fmt.Sprintf("%s:\\s+(.*)", regexKeys[i]))
|
||||
match := r.FindStringSubmatch(raw)
|
||||
|
||||
if len(match) > 1 {
|
||||
loads[i].Value = strings.TrimSpace(match[1])
|
||||
v, ok := raw[rawKeys[i]]
|
||||
if ok {
|
||||
loads = append(loads, types.NV{
|
||||
Name: dataKeys[i],
|
||||
Value: cast.ToString(v),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,13 +118,11 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Service) ErrorLog(w http.ResponseWriter, r *http.Request) {
|
||||
log, _ := shell.Execf("tail -n 500 %s/server/php/%d/var/log/php-fpm.log", app.Root, s.version)
|
||||
service.Success(w, log)
|
||||
service.Success(w, fmt.Sprintf("%s/server/php/%d/var/log/php-fpm.log", app.Root, s.version))
|
||||
}
|
||||
|
||||
func (s *Service) SlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
log, _ := shell.Execf("tail -n 500 %s/server/php/%d/var/log/slow.log", app.Root, s.version)
|
||||
service.Success(w, log)
|
||||
service.Success(w, fmt.Sprintf("%s/server/php/%d/var/log/slow.log", app.Root, s.version))
|
||||
}
|
||||
|
||||
func (s *Service) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -140,17 +145,6 @@ func (s *Service) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (s *Service) ExtensionList(w http.ResponseWriter, r *http.Request) {
|
||||
extensions := s.getExtensions()
|
||||
|
||||
// ionCube 只支持 PHP 8.3 以下版本
|
||||
if cast.ToUint(s.version) < 83 {
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的 PHP 加密解密工具。",
|
||||
Installed: false,
|
||||
})
|
||||
}
|
||||
|
||||
raw, err := shell.Execf("%s/server/php/%d/bin/php -m", app.Root, s.version)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -184,10 +178,10 @@ func (s *Service) InstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s.sh" | bash -s -- "install" "%d" >> /tmp/%s.log 2>&1`, url.QueryEscape(req.Slug), s.version, req.Slug)
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/%s.sh' | bash -s -- 'install' '%d' >> '/tmp/%s.log' 2>&1`, url.PathEscape(req.Slug), s.version, req.Slug)
|
||||
officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"}
|
||||
if slices.Contains(officials, req.Slug) {
|
||||
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/official.sh" | bash -s -- "install" "%d" "%s" >> /tmp/%s.log 2>&1`, s.version, req.Slug, req.Slug)
|
||||
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/official.sh' | bash -s -- 'install' '%d' '%s' >> '/tmp/%s.log' 2>&1`, s.version, req.Slug, req.Slug)
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
@@ -215,10 +209,10 @@ func (s *Service) UninstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/%s.sh" | bash -s -- "uninstall" "%d" >> /tmp/%s.log 2>&1`, url.QueryEscape(req.Slug), s.version, req.Slug)
|
||||
cmd := fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/%s.sh' | bash -s -- 'uninstall' '%d' >> '/tmp/%s.log' 2>&1`, url.PathEscape(req.Slug), s.version, req.Slug)
|
||||
officials := []string{"fileinfo", "exif", "imap", "pdo_pgsql", "zip", "bz2", "readline", "snmp", "ldap", "enchant", "pspell", "calendar", "gmp", "sysvmsg", "sysvsem", "sysvshm", "xsl", "intl", "gettext"}
|
||||
if slices.Contains(officials, req.Slug) {
|
||||
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 "https://dl.cdn.haozi.net/panel/php_exts/official.sh" | bash -s -- "uninstall" "%d" "%s" >> /tmp/%s.log 2>&1`, s.version, req.Slug, req.Slug)
|
||||
cmd = fmt.Sprintf(`curl -fsLm 10 --retry 3 'https://dl.cdn.haozi.net/panel/php_exts/official.sh' | bash -s -- 'uninstall' '%d' '%s' >> '/tmp/%s.log' 2>&1`, s.version, req.Slug, req.Slug)
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
@@ -239,137 +233,150 @@ func (s *Service) getExtensions() []Extension {
|
||||
{
|
||||
Name: "fileinfo",
|
||||
Slug: "fileinfo",
|
||||
Description: "Fileinfo 是一个用于识别文件类型的库。",
|
||||
Description: "Fileinfo 是一个用于识别文件类型的库",
|
||||
},
|
||||
{
|
||||
Name: "OPcache",
|
||||
Slug: "Zend OPcache",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销。",
|
||||
Description: "OPcache 通过将 PHP 脚本预编译的字节码存储到共享内存中来提升 PHP 的性能,存储预编译字节码可以省去每次加载和解析 PHP 脚本的开销",
|
||||
},
|
||||
{
|
||||
Name: "PhpRedis",
|
||||
Name: "Redis",
|
||||
Slug: "redis",
|
||||
Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据。",
|
||||
Description: "PhpRedis 是一个用 C 语言编写的 PHP 模块,用来连接并操作 Redis 数据库上的数据",
|
||||
},
|
||||
{
|
||||
Name: "Memcached",
|
||||
Slug: "memcached",
|
||||
Description: "Memcached 使用 libmemcached 库连接 Memcached 服务器",
|
||||
},
|
||||
{
|
||||
Name: "ImageMagick",
|
||||
Slug: "imagick",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件。",
|
||||
Description: "ImageMagick 是一个免费的创建、编辑、合成图片的软件",
|
||||
},
|
||||
{
|
||||
Name: "exif",
|
||||
Slug: "exif",
|
||||
Description: "通过 exif 扩展,你可以操作图像元数据。",
|
||||
Description: "通过 exif 扩展,您可以操作图像元数据",
|
||||
},
|
||||
{
|
||||
Name: "pdo_pgsql",
|
||||
Slug: "pdo_pgsql",
|
||||
Description: "(需先安装PostgreSQL)pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问。",
|
||||
Description: "pdo_pgsql 是一个驱动程序,它实现了 PHP 数据对象(PDO)接口以启用从 PHP 到 PostgreSQL 数据库的访问(需先安装PostgreSQL)",
|
||||
},
|
||||
{
|
||||
Name: "imap",
|
||||
Slug: "imap",
|
||||
Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件。",
|
||||
Description: "IMAP 扩展允许 PHP 读取、搜索、删除、下载和管理邮件",
|
||||
},
|
||||
{
|
||||
Name: "zip",
|
||||
Slug: "zip",
|
||||
Description: "Zip 是一个用于处理 ZIP 文件的库。",
|
||||
Description: "Zip 是一个用于处理 ZIP 文件的库",
|
||||
},
|
||||
{
|
||||
Name: "bz2",
|
||||
Slug: "bz2",
|
||||
Description: "Bzip2 是一个用于压缩和解压缩文件的库。",
|
||||
Description: "Bzip2 是一个用于压缩和解压缩文件的库",
|
||||
},
|
||||
{
|
||||
Name: "readline",
|
||||
Slug: "readline",
|
||||
Description: "Readline 是一个库,它提供了一种用于处理文本的接口。",
|
||||
Description: "Readline 是一个库,它提供了一种用于处理文本的接口",
|
||||
},
|
||||
{
|
||||
Name: "snmp",
|
||||
Slug: "snmp",
|
||||
Description: "SNMP 是一种用于网络管理的协议。",
|
||||
Description: "SNMP 是一种用于网络管理的协议",
|
||||
},
|
||||
{
|
||||
Name: "ldap",
|
||||
Slug: "ldap",
|
||||
Description: "LDAP 是一种用于访问目录服务的协议。",
|
||||
Description: "LDAP 是一种用于访问目录服务的协议",
|
||||
},
|
||||
{
|
||||
Name: "enchant",
|
||||
Slug: "enchant",
|
||||
Description: "Enchant 是一个拼写检查库。",
|
||||
Description: "Enchant 是一个拼写检查库",
|
||||
},
|
||||
{
|
||||
Name: "pspell",
|
||||
Slug: "pspell",
|
||||
Description: "Pspell 是一个拼写检查库。",
|
||||
Description: "Pspell 是一个拼写检查库",
|
||||
},
|
||||
{
|
||||
Name: "calendar",
|
||||
Slug: "calendar",
|
||||
Description: "Calendar 是一个用于处理日期的库。",
|
||||
Description: "Calendar 是一个用于处理日期的库",
|
||||
},
|
||||
{
|
||||
Name: "gmp",
|
||||
Slug: "gmp",
|
||||
Description: "GMP 是一个用于处理大整数的库。",
|
||||
Description: "GMP 是一个用于处理大整数的库",
|
||||
},
|
||||
{
|
||||
Name: "sysvmsg",
|
||||
Slug: "sysvmsg",
|
||||
Description: "Sysvmsg 是一个用于处理 System V 消息队列的库。",
|
||||
Description: "Sysvmsg 是一个用于处理 System V 消息队列的库",
|
||||
},
|
||||
{
|
||||
Name: "sysvsem",
|
||||
Slug: "sysvsem",
|
||||
Description: "Sysvsem 是一个用于处理 System V 信号量的库。",
|
||||
Description: "Sysvsem 是一个用于处理 System V 信号量的库",
|
||||
},
|
||||
{
|
||||
Name: "sysvshm",
|
||||
Slug: "sysvshm",
|
||||
Description: "Sysvshm 是一个用于处理 System V 共享内存的库。",
|
||||
Description: "Sysvshm 是一个用于处理 System V 共享内存的库",
|
||||
},
|
||||
{
|
||||
Name: "xsl",
|
||||
Slug: "xsl",
|
||||
Description: "XSL 是一个用于处理 XML 文档的库。",
|
||||
Description: "XSL 是一个用于处理 XML 文档的库",
|
||||
},
|
||||
{
|
||||
Name: "intl",
|
||||
Slug: "intl",
|
||||
Description: "Intl 是一个用于处理国际化和本地化的库。",
|
||||
Description: "Intl 是一个用于处理国际化和本地化的库",
|
||||
},
|
||||
{
|
||||
Name: "gettext",
|
||||
Slug: "gettext",
|
||||
Description: "Gettext 是一个用于处理多语言的库。",
|
||||
Description: "Gettext 是一个用于处理多语言的库",
|
||||
},
|
||||
{
|
||||
Name: "igbinary",
|
||||
Slug: "igbinary",
|
||||
Description: "Igbinary 是一个用于序列化和反序列化数据的库。",
|
||||
},
|
||||
{
|
||||
Name: "Swoole",
|
||||
Slug: "swoole",
|
||||
Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展。",
|
||||
},
|
||||
{
|
||||
Name: "Swow",
|
||||
Slug: "Swow",
|
||||
Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展。",
|
||||
Description: "Igbinary 是一个用于序列化和反序列化数据的库",
|
||||
},
|
||||
}
|
||||
|
||||
// ionCube 只支持 PHP 8.3 以下版本
|
||||
if cast.ToUint(s.version) < 83 {
|
||||
// ionCube Swoole 不支持 PHP 8.4
|
||||
if cast.ToUint(s.version) < 84 {
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "ionCube",
|
||||
Slug: "ionCube Loader",
|
||||
Description: "ionCube 是一个专业级的 PHP 加密解密工具。",
|
||||
Installed: false,
|
||||
Description: "ionCube 是一个专业级的 PHP 加密解密工具(需在 OPcache 之后安装)",
|
||||
})
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "Swoole",
|
||||
Slug: "swoole",
|
||||
Description: "Swoole 是一个用于构建高性能的异步并发服务器的 PHP 扩展",
|
||||
})
|
||||
}
|
||||
// Swow 不支持 PHP 8.0 以下版本且目前不支持 PHP 8.4
|
||||
if cast.ToUint(s.version) >= 80 && cast.ToUint(s.version) < 84 {
|
||||
extensions = append(extensions, Extension{
|
||||
Name: "Swow",
|
||||
Slug: "Swow",
|
||||
Description: "Swow 是一个用于构建高性能的异步并发服务器的 PHP 扩展",
|
||||
})
|
||||
}
|
||||
// PHP 8.4 移除了 pspell 和 imap 并且不再建议使用
|
||||
if cast.ToUint(s.version) >= 84 {
|
||||
extensions = slices.DeleteFunc(extensions, func(extension Extension) bool {
|
||||
return extension.Slug == "pspell" || extension.Slug == "imap"
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -78,8 +78,11 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
fw := firewall.NewFirewall()
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Port: req.Port,
|
||||
Protocol: "tcp",
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Direction: firewall.DirectionIn,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -132,8 +132,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Log 获取日志
|
||||
func (s *Service) Log(w http.ResponseWriter, r *http.Request) {
|
||||
log, _ := shell.Execf("tail -n 100 %s/server/postgresql/logs/postgresql-%s.log", app.Root, time.Now().Format(time.DateOnly))
|
||||
service.Success(w, log)
|
||||
service.Success(w, fmt.Sprintf("%s/server/postgresql/logs/postgresql-%s.log", app.Root, time.Now().Format(time.DateOnly)))
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
|
||||
@@ -2,7 +2,7 @@ package pureftpd
|
||||
|
||||
type Create struct {
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ type Delete struct {
|
||||
|
||||
type ChangePassword struct {
|
||||
Username string `form:"username" json:"username" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
}
|
||||
|
||||
type UpdatePort struct {
|
||||
Port uint `form:"port" json:"port" validate:"required"`
|
||||
Port uint `form:"port" json:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
}
|
||||
|
||||
@@ -156,8 +156,11 @@ func (s *Service) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
fw := firewall.NewFirewall()
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Port: req.Port,
|
||||
Protocol: "tcp",
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: req.Port,
|
||||
PortEnd: req.Port,
|
||||
Direction: firewall.DirectionIn,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-rat/chix"
|
||||
"github.com/go-rat/utils/str"
|
||||
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
@@ -114,11 +114,11 @@ secrets file = /etc/rsyncd.secrets
|
||||
# ` + req.Name + `-END
|
||||
`
|
||||
|
||||
if err = io.WriteAppend("/etc/rsyncd.conf", conf); err != nil {
|
||||
if err = io.WriteAppend("/etc/rsyncd.conf", conf, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.WriteAppend("/etc/rsyncd.secrets", fmt.Sprintf(`%s:%s\n`, req.AuthUser, req.Secret)); err != nil {
|
||||
if err = io.WriteAppend("/etc/rsyncd.secrets", fmt.Sprintf(`%s:%s\n`, req.AuthUser, req.Secret), 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -216,7 +216,7 @@ secrets file = /etc/rsyncd.secrets
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.WriteAppend("/etc/rsyncd.secrets", fmt.Sprintf(`%s:%s\n`, req.AuthUser, req.Secret)); err != nil {
|
||||
if err = io.WriteAppend("/etc/rsyncd.secrets", fmt.Sprintf(`%s:%s\n`, req.AuthUser, req.Secret), 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ func init() {
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/service", service.Service)
|
||||
r.Get("/log", service.Log)
|
||||
r.Post("/clearLog", service.ClearLog)
|
||||
r.Get("/config", service.GetConfig)
|
||||
r.Post("/config", service.UpdateConfig)
|
||||
|
||||
@@ -36,12 +36,6 @@ func (s *Service) Service(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, s.name)
|
||||
}
|
||||
|
||||
// Log 日志
|
||||
func (s *Service) Log(w http.ResponseWriter, r *http.Request) {
|
||||
log, _ := shell.Execf(`tail -n 200 /var/log/supervisor/supervisord.log`)
|
||||
service.Success(w, log)
|
||||
}
|
||||
|
||||
// ClearLog 清空日志
|
||||
func (s *Service) ClearLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf(`echo "" > /var/log/supervisor/supervisord.log`); err != nil {
|
||||
@@ -204,8 +198,7 @@ func (s *Service) ProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
log, _ := shell.Execf(`tail -n 200 '%s'`, logPath)
|
||||
service.Success(w, log)
|
||||
service.Success(w, logPath)
|
||||
}
|
||||
|
||||
// ClearProcessLog 清空进程日志
|
||||
|
||||
@@ -18,6 +18,10 @@ func init() {
|
||||
r.Post("/swap", service.UpdateSWAP)
|
||||
r.Get("/timezone", service.GetTimezone)
|
||||
r.Post("/timezone", service.UpdateTimezone)
|
||||
r.Post("/time", service.UpdateTime)
|
||||
r.Post("/syncTime", service.SyncTime)
|
||||
r.Get("/hostname", service.GetHostname)
|
||||
r.Post("/hostname", service.UpdateHostname)
|
||||
r.Get("/hosts", service.GetHosts)
|
||||
r.Post("/hosts", service.UpdateHosts)
|
||||
r.Post("/rootPassword", service.UpdateRootPassword)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package toolbox
|
||||
|
||||
import "time"
|
||||
|
||||
type DNS struct {
|
||||
DNS1 string `form:"dns1" json:"dns1" validate:"required"`
|
||||
DNS2 string `form:"dns2" json:"dns2" validate:"required"`
|
||||
@@ -13,10 +15,18 @@ type Timezone struct {
|
||||
Timezone string `form:"timezone" json:"timezone" validate:"required"`
|
||||
}
|
||||
|
||||
type Time struct {
|
||||
Time time.Time `form:"time" json:"time" validate:"required"`
|
||||
}
|
||||
|
||||
type Hostname struct {
|
||||
Hostname string `form:"hostname" json:"hostname" validate:"required,regex=^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$"`
|
||||
}
|
||||
|
||||
type Hosts struct {
|
||||
Hosts string `form:"hosts" json:"hosts"`
|
||||
}
|
||||
|
||||
type Password struct {
|
||||
Password string `form:"password" json:"password" validate:"required"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@ import (
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/service"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/ntp"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
@@ -76,7 +77,7 @@ func (s *Service) GetSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
size = file.Size() / 1024 / 1024
|
||||
total = str.FormatBytes(float64(file.Size()))
|
||||
total = tools.FormatBytes(float64(file.Size()))
|
||||
} else {
|
||||
size = 0
|
||||
total = "0.00 B"
|
||||
@@ -90,8 +91,8 @@ func (s *Service) GetSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
match := regexp.MustCompile(`Swap:\s+(\d+)\s+(\d+)\s+(\d+)`).FindStringSubmatch(raw)
|
||||
if len(match) >= 4 {
|
||||
used = str.FormatBytes(cast.ToFloat64(match[2]) * 1024)
|
||||
free = str.FormatBytes(cast.ToFloat64(match[3]) * 1024)
|
||||
used = tools.FormatBytes(cast.ToFloat64(match[2]) * 1024)
|
||||
free = tools.FormatBytes(cast.ToFloat64(match[3]) * 1024)
|
||||
}
|
||||
|
||||
service.Success(w, chix.M{
|
||||
@@ -133,7 +134,7 @@ func (s *Service) UpdateSWAP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if cast.ToInt64(free)*1024 < req.Size*1024*1024 {
|
||||
service.Error(w, http.StatusInternalServerError, "硬盘空间不足,当前剩余%s", str.FormatBytes(cast.ToFloat64(free)))
|
||||
service.Error(w, http.StatusInternalServerError, "硬盘空间不足,当前剩余%s", tools.FormatBytes(cast.ToFloat64(free)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -221,6 +222,64 @@ func (s *Service) UpdateTimezone(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdateTime 设置时间
|
||||
func (s *Service) UpdateTime(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Time](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ntp.UpdateSystemTime(req.Time); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
|
||||
}
|
||||
|
||||
// SyncTime 同步时间
|
||||
func (s *Service) SyncTime(w http.ResponseWriter, r *http.Request) {
|
||||
now, err := ntp.Now()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ntp.UpdateSystemTime(now); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetHostname 获取主机名
|
||||
func (s *Service) GetHostname(w http.ResponseWriter, r *http.Request) {
|
||||
hostname, _ := io.Read("/etc/hostname")
|
||||
service.Success(w, strings.TrimSpace(hostname))
|
||||
}
|
||||
|
||||
// UpdateHostname 设置主机名
|
||||
func (s *Service) UpdateHostname(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Hostname](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("hostnamectl set-hostname '%s'", req.Hostname); err != nil {
|
||||
// 直接写 /etc/hostname
|
||||
if err = io.Write("/etc/hostname", req.Hostname, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "写入主机名失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetHosts 获取 hosts 信息
|
||||
func (s *Service) GetHosts(w http.ResponseWriter, r *http.Request) {
|
||||
hosts, _ := io.Read("/etc/hosts")
|
||||
@@ -251,11 +310,6 @@ func (s *Service) UpdateRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`^[a-zA-Z0-9·~!@#$%^&*()_+-=\[\]{};:'",./<>?]{6,20}$`).MatchString(req.Password) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "密码必须为 6-20 位字母、数字或特殊字符")
|
||||
return
|
||||
}
|
||||
|
||||
req.Password = strings.ReplaceAll(req.Password, `'`, `\'`)
|
||||
if _, err = shell.Execf(`yes '%s' | passwd root`, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
|
||||
@@ -30,5 +30,4 @@ type AppRepo interface {
|
||||
UnInstall(slug string) error
|
||||
Update(slug string) error
|
||||
UpdateShow(slug string, show bool) error
|
||||
UpdateCache() error
|
||||
}
|
||||
|
||||
@@ -19,4 +19,6 @@ type Cache struct {
|
||||
type CacheRepo interface {
|
||||
Get(key CacheKey, defaultValue ...string) (string, error)
|
||||
Set(key CacheKey, value string) error
|
||||
UpdateApps() error
|
||||
UpdateRewrites() error
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/acme"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type Cert struct {
|
||||
@@ -12,7 +13,7 @@ type Cert struct {
|
||||
AccountID uint `gorm:"not null" json:"account_id"` // 关联的 ACME 账户 ID
|
||||
WebsiteID uint `gorm:"not null" json:"website_id"` // 关联的网站 ID
|
||||
DNSID uint `gorm:"not null" json:"dns_id"` // 关联的 DNS ID
|
||||
Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 4096)
|
||||
Type string `gorm:"not null" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096)
|
||||
Domains []string `gorm:"not null;serializer:json" json:"domains"`
|
||||
AutoRenew bool `gorm:"not null" json:"auto_renew"` // 自动续签
|
||||
CertURL string `gorm:"not null" json:"cert_url"` // 证书 URL (续签时使用)
|
||||
@@ -27,13 +28,16 @@ type Cert struct {
|
||||
}
|
||||
|
||||
type CertRepo interface {
|
||||
List(page, limit uint) ([]*Cert, int64, error)
|
||||
List(page, limit uint) ([]*types.CertList, int64, error)
|
||||
Get(id uint) (*Cert, error)
|
||||
GetByWebsite(WebsiteID uint) (*Cert, error)
|
||||
Upload(req *request.CertUpload) (*Cert, error)
|
||||
Create(req *request.CertCreate) (*Cert, error)
|
||||
Update(req *request.CertUpdate) error
|
||||
Delete(id uint) error
|
||||
ObtainAuto(id uint) (*acme.Certificate, error)
|
||||
ObtainManual(id uint) (*acme.Certificate, error)
|
||||
ObtainSelfSigned(id uint) error
|
||||
Renew(id uint) (*acme.Certificate, error)
|
||||
ManualDNS(id uint) ([]acme.DNSRecord, error)
|
||||
Deploy(ID, WebsiteID uint) error
|
||||
|
||||
@@ -13,7 +13,7 @@ type CertAccount struct {
|
||||
Kid string `gorm:"not null" json:"kid"`
|
||||
HmacEncoded string `gorm:"not null" json:"hmac_encoded"`
|
||||
PrivateKey string `gorm:"not null" json:"private_key"`
|
||||
KeyType string `gorm:"not null" json:"key_type"`
|
||||
KeyType string `gorm:"not null" json:"key_type"` // 密钥类型 (P256, P384, 2048, 3072, 4096)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
@@ -22,6 +22,7 @@ type CertAccount struct {
|
||||
|
||||
type CertAccountRepo interface {
|
||||
List(page, limit uint) ([]*CertAccount, int64, error)
|
||||
GetDefault(userID uint) (*CertAccount, error)
|
||||
Get(id uint) (*CertAccount, error)
|
||||
Create(req *request.CertAccountCreate) (*CertAccount, error)
|
||||
Update(req *request.CertAccountUpdate) error
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
type CertDNS struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null" json:"name"` // 备注名称
|
||||
Type string `gorm:"not null" json:"type"` // DNS 提供商 (dnspod, tencent, aliyun, cloudflare)
|
||||
Type string `gorm:"not null" json:"type"` // DNS 提供商 (tencent, aliyun, cloudflare)
|
||||
Data acme.DNSParam `gorm:"not null;serializer:json" json:"dns_param"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type ContainerRepo interface {
|
||||
ListAll() ([]types.Container, error)
|
||||
ListByNames(names []string) ([]types.Container, error)
|
||||
ListByName(name string) ([]types.Container, error)
|
||||
Create(req *request.ContainerCreate) (string, error)
|
||||
Remove(id string) error
|
||||
Start(id string) error
|
||||
@@ -17,12 +15,8 @@ type ContainerRepo interface {
|
||||
Restart(id string) error
|
||||
Pause(id string) error
|
||||
Unpause(id string) error
|
||||
Inspect(id string) (types.ContainerJSON, error)
|
||||
Kill(id string) error
|
||||
Rename(id string, newName string) error
|
||||
Stats(id string) (container.StatsResponseReader, error)
|
||||
Exist(name string) (bool, error)
|
||||
Update(id string, config container.UpdateConfig) error
|
||||
Logs(id string) (string, error)
|
||||
Prune() error
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type ContainerImageRepo interface {
|
||||
List() ([]image.Summary, error)
|
||||
Exist(id string) (bool, error)
|
||||
List() ([]types.ContainerImage, error)
|
||||
Pull(req *request.ContainerImagePull) error
|
||||
Remove(id string) error
|
||||
Prune() error
|
||||
Inspect(id string) (types.ImageInspect, error)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/network"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type ContainerNetworkRepo interface {
|
||||
List() ([]network.Inspect, error)
|
||||
List() ([]types.ContainerNetwork, error)
|
||||
Create(req *request.ContainerNetworkCreate) (string, error)
|
||||
Remove(id string) error
|
||||
Exist(name string) (bool, error)
|
||||
Inspect(id string) (network.Inspect, error)
|
||||
Connect(networkID string, containerID string) error
|
||||
Disconnect(networkID string, containerID string) error
|
||||
Prune() error
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type ContainerVolumeRepo interface {
|
||||
List() ([]*volume.Volume, error)
|
||||
Create(req *request.ContainerVolumeCreate) (volume.Volume, error)
|
||||
Exist(name string) (bool, error)
|
||||
Inspect(id string) (volume.Volume, error)
|
||||
List() ([]types.ContainerVolume, error)
|
||||
Create(req *request.ContainerVolumeCreate) (string, error)
|
||||
Remove(id string) error
|
||||
Prune() error
|
||||
}
|
||||
|
||||
@@ -26,5 +26,4 @@ type CronRepo interface {
|
||||
Update(req *request.CronUpdate) error
|
||||
Delete(id uint) error
|
||||
Status(id uint, status bool) error
|
||||
Log(id uint) (string, error)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
package biz
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type DatabaseType string
|
||||
|
||||
const (
|
||||
DatabaseTypeMysql DatabaseType = "mysql"
|
||||
DatabaseTypePostgresql DatabaseType = "postgresql"
|
||||
DatabaseTypeMongoDB DatabaseType = "mongodb"
|
||||
DatabaseSQLite DatabaseType = "sqlite"
|
||||
DatabaseTypeRedis DatabaseType = "redis"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;unique" json:"name"`
|
||||
Type string `gorm:"not null" 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:"not null" json:"password"`
|
||||
Remark string `gorm:"not null" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Type DatabaseType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Server string `json:"server"`
|
||||
ServerID uint `json:"server_id"`
|
||||
Encoding string `json:"encoding"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type DatabaseRepo interface {
|
||||
List(page, limit uint) ([]*Database, int64, error)
|
||||
Create(req *request.DatabaseCreate) error
|
||||
Delete(serverID uint, name string) error
|
||||
Comment(req *request.DatabaseComment) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type DatabaseServerStatus string
|
||||
|
||||
const (
|
||||
DatabaseServerStatusValid DatabaseServerStatus = "valid"
|
||||
DatabaseServerStatusInvalid DatabaseServerStatus = "invalid"
|
||||
)
|
||||
|
||||
type DatabaseServer struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null;unique" json:"name"`
|
||||
Type DatabaseType `gorm:"not null" json:"type"`
|
||||
Host string `gorm:"not null" json:"host"`
|
||||
Port uint `gorm:"not null" json:"port"`
|
||||
Username string `gorm:"not null" json:"username"`
|
||||
Password string `gorm:"not null" json:"password"`
|
||||
Status DatabaseServerStatus `gorm:"-:all" json:"status"`
|
||||
Remark string `gorm:"not null" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (r *DatabaseServer) BeforeSave(tx *gorm.DB) error {
|
||||
var err error
|
||||
r.Password, err = app.Crypter.Encrypt([]byte(r.Password))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *DatabaseServer) AfterFind(tx *gorm.DB) error {
|
||||
password, err := app.Crypter.Decrypt(r.Password)
|
||||
if err == nil {
|
||||
r.Password = string(password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DatabaseServerRepo interface {
|
||||
Count() (int64, error)
|
||||
List(page, limit uint) ([]*DatabaseServer, int64, error)
|
||||
Get(id uint) (*DatabaseServer, error)
|
||||
GetByName(name string) (*DatabaseServer, error)
|
||||
Create(req *request.DatabaseServerCreate) error
|
||||
Update(req *request.DatabaseServerUpdate) error
|
||||
UpdateRemark(req *request.DatabaseServerUpdateRemark) error
|
||||
Delete(id uint) error
|
||||
Sync(id uint) error
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type DatabaseUserStatus string
|
||||
|
||||
const (
|
||||
DatabaseUserStatusValid DatabaseUserStatus = "valid"
|
||||
DatabaseUserStatusInvalid DatabaseUserStatus = "invalid"
|
||||
)
|
||||
|
||||
type DatabaseUser struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ServerID uint `gorm:"not null" json:"server_id"`
|
||||
Username string `gorm:"not null" json:"username"`
|
||||
Password string `gorm:"not null" json:"password"`
|
||||
Host string `gorm:"not null" json:"host"` // 仅 mysql
|
||||
Status DatabaseUserStatus `gorm:"-:all" json:"status"` // 仅显示
|
||||
Privileges []string `gorm:"-:all" json:"privileges"` // 仅显示
|
||||
Remark string `gorm:"not null" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
Server *DatabaseServer `gorm:"foreignKey:ServerID;references:ID" json:"server"`
|
||||
}
|
||||
|
||||
func (r *DatabaseUser) BeforeSave(tx *gorm.DB) error {
|
||||
var err error
|
||||
r.Password, err = app.Crypter.Encrypt([]byte(r.Password))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *DatabaseUser) AfterFind(tx *gorm.DB) error {
|
||||
password, err := app.Crypter.Decrypt(r.Password)
|
||||
if err == nil {
|
||||
r.Password = string(password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type DatabaseUserRepo interface {
|
||||
Count() (int64, error)
|
||||
List(page, limit uint) ([]*DatabaseUser, int64, error)
|
||||
Get(id uint) (*DatabaseUser, error)
|
||||
Create(req *request.DatabaseUserCreate) error
|
||||
Update(req *request.DatabaseUserUpdate) error
|
||||
UpdateRemark(req *request.DatabaseUserUpdateRemark) error
|
||||
Delete(id uint) error
|
||||
DeleteByNames(serverID uint, names []string) error
|
||||
DeleteByServerID(serverID uint) error
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package biz
|
||||
|
||||
type FirewallRepo interface {
|
||||
}
|
||||
@@ -4,14 +4,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type Monitor struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Info tools.MonitoringInfo `gorm:"not null;serializer:json" json:"info"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Info types.CurrentInfo `gorm:"not null;serializer:json" json:"info"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type MonitorRepo interface {
|
||||
|
||||
@@ -17,10 +17,7 @@ const (
|
||||
SettingKeyBackupPath SettingKey = "backup_path"
|
||||
SettingKeyWebsitePath SettingKey = "website_path"
|
||||
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
|
||||
SettingKeySshHost SettingKey = "ssh_host"
|
||||
SettingKeySshPort SettingKey = "ssh_port"
|
||||
SettingKeySshUser SettingKey = "ssh_user"
|
||||
SettingKeySshPassword SettingKey = "ssh_password"
|
||||
SettingKeyOfflineMode SettingKey = "offline_mode"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
@@ -33,6 +30,7 @@ type Setting struct {
|
||||
|
||||
type SettingRepo interface {
|
||||
Get(key SettingKey, defaultValue ...string) (string, error)
|
||||
GetBool(key SettingKey, defaultValue ...bool) (bool, error)
|
||||
Set(key SettingKey, value string) error
|
||||
Delete(key SettingKey) error
|
||||
GetPanelSetting(ctx context.Context) (*request.PanelSetting, error)
|
||||
|
||||
@@ -1,8 +1,58 @@
|
||||
package biz
|
||||
|
||||
import "github.com/TheTNB/panel/internal/http/request"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/ssh"
|
||||
)
|
||||
|
||||
type SSH struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Host string `gorm:"not null" json:"host"`
|
||||
Port uint `gorm:"not null" json:"port"`
|
||||
Config ssh.ClientConfig `gorm:"not null;serializer:json" json:"config"`
|
||||
Remark string `gorm:"not null" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (r *SSH) BeforeSave(tx *gorm.DB) error {
|
||||
var err error
|
||||
r.Config.Key, err = app.Crypter.Encrypt([]byte(r.Config.Key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Config.Password, err = app.Crypter.Encrypt([]byte(r.Config.Password))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *SSH) AfterFind(tx *gorm.DB) error {
|
||||
key, err := app.Crypter.Decrypt(r.Config.Key)
|
||||
if err == nil {
|
||||
r.Config.Key = string(key)
|
||||
}
|
||||
password, err := app.Crypter.Decrypt(r.Config.Password)
|
||||
if err == nil {
|
||||
r.Config.Password = string(password)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type SSHRepo interface {
|
||||
GetInfo() (map[string]any, error)
|
||||
UpdateInfo(req *request.SSHUpdateInfo) error
|
||||
List(page, limit uint) ([]*SSH, int64, error)
|
||||
Get(id uint) (*SSH, error)
|
||||
Create(req *request.SSHCreate) error
|
||||
Update(req *request.SSHUpdate) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
@@ -28,5 +28,5 @@ type TaskRepo interface {
|
||||
Delete(id uint) error
|
||||
UpdateStatus(id uint, status TaskStatus) error
|
||||
Push(task *Task) error
|
||||
DispatchWaiting() error
|
||||
DispatchWaiting()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
@@ -21,6 +22,7 @@ type Website struct {
|
||||
}
|
||||
|
||||
type WebsiteRepo interface {
|
||||
GetRewrites() (map[string]string, error)
|
||||
UpdateDefaultConfig(req *request.WebsiteDefaultConfig) error
|
||||
Count() (int64, error)
|
||||
Get(id uint) (*types.WebsiteSetting, error)
|
||||
@@ -33,4 +35,5 @@ type WebsiteRepo interface {
|
||||
UpdateRemark(id uint, remark string) error
|
||||
ResetConfig(id uint) error
|
||||
UpdateStatus(id uint, status bool) error
|
||||
ObtainCert(ctx context.Context, id uint) error
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
func boot() {
|
||||
if os.Geteuid() != 0 {
|
||||
panic("panel must run as root")
|
||||
}
|
||||
|
||||
debug.SetGCPercent(10)
|
||||
debug.SetMemoryLimit(64 << 20)
|
||||
|
||||
@@ -13,14 +18,15 @@ func boot() {
|
||||
initLogger()
|
||||
initOrm()
|
||||
runMigrate()
|
||||
bootCrypter()
|
||||
}
|
||||
|
||||
func BootWeb() {
|
||||
boot()
|
||||
initValidator()
|
||||
initSession()
|
||||
initCron()
|
||||
initQueue()
|
||||
initCron()
|
||||
initHttp()
|
||||
|
||||
select {}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/urfave/cli/v3"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
@@ -14,6 +13,8 @@ import (
|
||||
)
|
||||
|
||||
func initCli() {
|
||||
app.IsCli = true
|
||||
|
||||
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "NAME", "名称")
|
||||
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "USAGE", "用法")
|
||||
cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "VERSION", "版本")
|
||||
@@ -34,7 +35,7 @@ func initCli() {
|
||||
cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "OPTIONS", "选项")
|
||||
|
||||
cli.RootCommandHelpTemplate += "\n官网:https://panel.haozi.net"
|
||||
cli.RootCommandHelpTemplate += "\n论坛:https://tom.moe"
|
||||
cli.RootCommandHelpTemplate += "\n论坛:https://bbs.haozi.net"
|
||||
cli.RootCommandHelpTemplate += "\nQ群:12370907\n"
|
||||
|
||||
cmd := &cli.Command{
|
||||
@@ -44,6 +45,6 @@ func initCli() {
|
||||
Commands: route.Cli(),
|
||||
}
|
||||
if err := cmd.Run(context.Background(), os.Args); err != nil {
|
||||
color.Redln(fmt.Sprintf("|-%v", err))
|
||||
fmt.Printf("|-%v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,12 @@ func initConf() {
|
||||
}
|
||||
|
||||
func initGlobal() {
|
||||
app.Key = app.Conf.MustString("app.key")
|
||||
if len(app.Key) != 32 {
|
||||
log.Fatalf("app key must be 32 characters")
|
||||
}
|
||||
|
||||
app.Root = app.Conf.MustString("app.root")
|
||||
app.Version = "2.3.5"
|
||||
app.Locale = app.Conf.MustString("app.locale")
|
||||
|
||||
// 初始化时区
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/go-rat/utils/crypt"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
)
|
||||
|
||||
func bootCrypter() {
|
||||
crypter, err := crypt.NewXChacha20Poly1305([]byte(app.Key))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create crypter: %v", err)
|
||||
}
|
||||
|
||||
app.Crypter = crypter
|
||||
}
|
||||
@@ -6,25 +6,16 @@ import (
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/go-gormigrate/gormigrate/v2"
|
||||
slogGorm "github.com/orandin/slog-gorm"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"moul.io/zapgorm2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/migration"
|
||||
)
|
||||
|
||||
func initOrm() {
|
||||
logLevel := logger.Error
|
||||
if app.Conf.Bool("database.debug") {
|
||||
logLevel = logger.Info
|
||||
}
|
||||
zapLogger := zapgorm2.New(app.Logger)
|
||||
zapLogger.LogMode(logLevel)
|
||||
zapLogger.SetAsDefault()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(app.Root, "panel/storage/app.db")), &gorm.Config{
|
||||
Logger: zapLogger,
|
||||
Logger: slogGorm.New(slogGorm.WithHandler(app.Logger.Handler())),
|
||||
SkipDefaultTransaction: true,
|
||||
DisableForeignKeyConstraintWhenMigrating: true,
|
||||
})
|
||||
@@ -36,8 +27,7 @@ func initOrm() {
|
||||
|
||||
func runMigrate() {
|
||||
migrator := gormigrate.New(app.Orm, &gormigrate.Options{
|
||||
UseTransaction: true, // Note: MySQL not support DDL transaction
|
||||
ValidateUnknownMigrations: true,
|
||||
UseTransaction: true, // Note: MySQL not support DDL transaction
|
||||
}, migration.Migrations)
|
||||
if err := migrator.Migrate(); err != nil {
|
||||
log.Fatalf("failed to migrate database: %v", err)
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/bddjr/hlfhr"
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/apps"
|
||||
"github.com/TheTNB/panel/internal/http/middleware"
|
||||
"github.com/TheTNB/panel/internal/route"
|
||||
)
|
||||
@@ -23,17 +23,20 @@ func initHttp() {
|
||||
|
||||
// add route
|
||||
route.Http(app.Http)
|
||||
apps.Boot(app.Http)
|
||||
route.Ws(app.Http)
|
||||
|
||||
srv := &http.Server{
|
||||
srv := hlfhr.New(&http.Server{
|
||||
Addr: fmt.Sprintf(":%d", app.Conf.MustInt("http.port")),
|
||||
Handler: http.AllowQuerySemicolons(app.Http),
|
||||
MaxHeaderBytes: 2048 << 20,
|
||||
}
|
||||
})
|
||||
srv.HttpOnHttpsPortErrorHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
hlfhr.RedirectToHttps(w, r, http.StatusTemporaryRedirect)
|
||||
})
|
||||
|
||||
if app.Conf.Bool("http.tls") {
|
||||
srv.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
|
||||
cert := filepath.Join(app.Root, "panel/storage/cert.pem")
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/exp/zapslog"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
|
||||
@@ -13,16 +15,15 @@ import (
|
||||
|
||||
func initLogger() {
|
||||
writeSyncer := zapcore.AddSync(&lumberjack.Logger{
|
||||
Filename: filepath.Join(app.Root, "panel/storage/logs/app.log"),
|
||||
MaxSize: 10,
|
||||
MaxBackups: 10,
|
||||
MaxAge: 30,
|
||||
Compress: true,
|
||||
Filename: filepath.Join(app.Root, "panel/storage/logs/app.log"),
|
||||
MaxSize: 10,
|
||||
MaxAge: 30,
|
||||
Compress: true,
|
||||
})
|
||||
|
||||
level := zapcore.InfoLevel
|
||||
level := zap.InfoLevel
|
||||
if app.Conf.Bool("app.debug") {
|
||||
level = zapcore.DebugLevel
|
||||
level = zap.DebugLevel
|
||||
}
|
||||
|
||||
config := zap.NewProductionEncoderConfig()
|
||||
@@ -34,8 +35,7 @@ func initLogger() {
|
||||
)
|
||||
|
||||
logger := zap.New(core)
|
||||
defer func(logger *zap.Logger) {
|
||||
_ = logger.Sync()
|
||||
}(logger)
|
||||
app.Logger = logger
|
||||
zap.ReplaceGlobals(logger)
|
||||
app.Logger = slog.New(zapslog.NewHandler(logger.Core()))
|
||||
slog.SetDefault(app.Logger)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ import (
|
||||
)
|
||||
|
||||
func initQueue() {
|
||||
app.Queue = queue.New(40)
|
||||
app.Queue = queue.New(100)
|
||||
go app.Queue.Run(context.Background())
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func initSession() {
|
||||
// initialize session manager
|
||||
manager, err := sessions.NewManager(&sessions.ManagerOptions{
|
||||
Key: app.Conf.String("app.key"),
|
||||
Key: app.Key,
|
||||
Lifetime: 120,
|
||||
GcInterval: 30,
|
||||
DisableDefaultDriver: true,
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/go-playground/validator/v10/translations/zh"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/http/rule"
|
||||
)
|
||||
|
||||
func initValidator() {
|
||||
@@ -23,4 +24,7 @@ func initValidator() {
|
||||
|
||||
app.Translator = &trans
|
||||
app.Validator = validate
|
||||
if err := rule.RegisterRules(validate); err != nil {
|
||||
log.Fatalf("failed to register validator rules: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,32 +7,25 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/expr-lang/expr"
|
||||
"github.com/go-rat/utils/collect"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
)
|
||||
|
||||
type appRepo struct {
|
||||
cacheRepo biz.CacheRepo
|
||||
taskRepo biz.TaskRepo
|
||||
api *api.API
|
||||
}
|
||||
type appRepo struct{}
|
||||
|
||||
func NewAppRepo() biz.AppRepo {
|
||||
return &appRepo{
|
||||
cacheRepo: NewCacheRepo(),
|
||||
taskRepo: NewTaskRepo(),
|
||||
api: api.NewAPI(app.Version),
|
||||
}
|
||||
return do.MustInvoke[biz.AppRepo](injector)
|
||||
}
|
||||
|
||||
func (r *appRepo) All() api.Apps {
|
||||
cached, err := r.cacheRepo.Get(biz.CacheKeyApps)
|
||||
cached, err := NewCacheRepo().Get(biz.CacheKeyApps)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -64,7 +57,7 @@ func (r *appRepo) UpdateExist(slug string) bool {
|
||||
|
||||
for channel := range slices.Values(item.Channels) {
|
||||
if channel.Slug == installed.Channel {
|
||||
current := str.FirstElement(channel.Subs)
|
||||
current := collect.First(channel.Subs)
|
||||
if current != nil && current.Version != installed.Version {
|
||||
return true
|
||||
}
|
||||
@@ -108,7 +101,7 @@ func (r *appRepo) GetHomeShow() ([]map[string]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var filtered []map[string]string
|
||||
filtered := make([]map[string]string, 0)
|
||||
for item := range slices.Values(apps) {
|
||||
loaded, err := r.Get(item.Slug)
|
||||
if err != nil {
|
||||
@@ -167,7 +160,7 @@ func (r *appRepo) Install(channel, slug string) error {
|
||||
}
|
||||
shellUrl = ch.Install
|
||||
shellChannel = ch.Slug
|
||||
shellVersion = str.FirstElement(ch.Subs).Version
|
||||
shellVersion = collect.First(ch.Subs).Version
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -179,16 +172,17 @@ func (r *appRepo) Install(channel, slug string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Name = "安装应用 " + item.Name
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err = r.taskRepo.Push(task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
return NewTaskRepo().Push(task)
|
||||
}
|
||||
|
||||
func (r *appRepo) UnInstall(slug string) error {
|
||||
@@ -233,16 +227,17 @@ func (r *appRepo) UnInstall(slug string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Name = "卸载应用 " + item.Name
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err = r.taskRepo.Push(task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
return NewTaskRepo().Push(task)
|
||||
}
|
||||
|
||||
func (r *appRepo) Update(slug string) error {
|
||||
@@ -275,7 +270,7 @@ func (r *appRepo) Update(slug string) error {
|
||||
}
|
||||
shellUrl = ch.Update
|
||||
shellChannel = ch.Slug
|
||||
shellVersion = str.FirstElement(ch.Subs).Version
|
||||
shellVersion = collect.First(ch.Subs).Version
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -287,16 +282,17 @@ func (r *appRepo) Update(slug string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
return shell.ExecfWithOutput(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s"`, shellUrl, shellChannel, shellVersion)
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Name = "更新应用 " + item.Name
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err = r.taskRepo.Push(task); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
return NewTaskRepo().Push(task)
|
||||
}
|
||||
|
||||
func (r *appRepo) UpdateShow(slug string, show bool) error {
|
||||
@@ -310,26 +306,6 @@ func (r *appRepo) UpdateShow(slug string, show bool) error {
|
||||
return app.Orm.Save(item).Error
|
||||
}
|
||||
|
||||
func (r *appRepo) UpdateCache() error {
|
||||
remote, err := r.api.Apps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 去除本地不存在的应用
|
||||
*remote = slices.Clip(slices.DeleteFunc(*remote, func(app *api.App) bool {
|
||||
_, err = apploader.Get(app.Slug)
|
||||
return err != nil
|
||||
}))
|
||||
|
||||
encoded, err := json.Marshal(remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.cacheRepo.Set(biz.CacheKeyApps, string(encoded))
|
||||
}
|
||||
|
||||
func (r *appRepo) preCheck(app *api.App) error {
|
||||
var apps []string
|
||||
var installed []string
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/shirou/gopsutil/disk"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
@@ -17,20 +17,14 @@ import (
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type backupRepo struct {
|
||||
setting biz.SettingRepo
|
||||
website biz.WebsiteRepo
|
||||
}
|
||||
type backupRepo struct{}
|
||||
|
||||
func NewBackupRepo() biz.BackupRepo {
|
||||
return &backupRepo{
|
||||
setting: NewSettingRepo(),
|
||||
website: NewWebsiteRepo(),
|
||||
}
|
||||
return do.MustInvoke[biz.BackupRepo](injector)
|
||||
}
|
||||
|
||||
// List 备份列表
|
||||
@@ -54,7 +48,7 @@ func (r *backupRepo) List(typ biz.BackupType) ([]*types.BackupFile, error) {
|
||||
list = append(list, &types.BackupFile{
|
||||
Name: file.Name(),
|
||||
Path: filepath.Join(path, file.Name()),
|
||||
Size: str.FormatBytes(float64(info.Size())),
|
||||
Size: tools.FormatBytes(float64(info.Size())),
|
||||
Time: info.ModTime(),
|
||||
})
|
||||
}
|
||||
@@ -135,7 +129,7 @@ func (r *backupRepo) CutoffLog(path, target string) error {
|
||||
}
|
||||
|
||||
to := filepath.Join(path, fmt.Sprintf("%s_%s.zip", time.Now().Format("20060102150405"), filepath.Base(target)))
|
||||
if err := io.Compress([]string{target}, to, io.Zip); err != nil {
|
||||
if err := io.Compress(filepath.Dir(target), []string{filepath.Base(target)}, to); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -181,9 +175,15 @@ func (r *backupRepo) ClearExpired(path, prefix string, save int) error {
|
||||
toDelete := filtered[save:]
|
||||
for _, file := range toDelete {
|
||||
filePath := filepath.Join(path, file.Name())
|
||||
color.Greenln(fmt.Sprintf("|-清理过期文件:%s", filePath))
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-清理过期文件:%s\n", filePath)
|
||||
}
|
||||
if err = os.Remove(filePath); err != nil {
|
||||
color.Redln(fmt.Sprintf("|-清理失败:%v", err))
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-清理失败:%v\n", err)
|
||||
} else {
|
||||
return fmt.Errorf("清理失败:%v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ func (r *backupRepo) ClearExpired(path, prefix string, save int) error {
|
||||
|
||||
// GetPath 获取备份路径
|
||||
func (r *backupRepo) GetPath(typ biz.BackupType) (string, error) {
|
||||
backupPath, err := r.setting.Get(biz.SettingKeyBackupPath)
|
||||
backupPath, err := NewSettingRepo().Get(biz.SettingKeyBackupPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -212,7 +212,7 @@ func (r *backupRepo) GetPath(typ biz.BackupType) (string, error) {
|
||||
|
||||
// createWebsite 创建网站备份
|
||||
func (r *backupRepo) createWebsite(to string, name string) error {
|
||||
website, err := r.website.GetByName(name)
|
||||
website, err := NewWebsiteRepo().GetByName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -221,33 +221,26 @@ func (r *backupRepo) createWebsite(to string, name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var paths []string
|
||||
dirs, err := io.ReadDir(website.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range dirs {
|
||||
paths = append(paths, filepath.Join(website.Path, item.Name()))
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
backup := filepath.Join(to, fmt.Sprintf("%s_%s.zip", website.Name, time.Now().Format("20060102150405")))
|
||||
if err = io.Compress(paths, backup, io.Zip); err != nil {
|
||||
if err = io.Compress(website.Path, nil, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup))
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
|
||||
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createMySQL 创建 MySQL 备份
|
||||
func (r *backupRepo) createMySQL(to string, name string) error {
|
||||
rootPassword, err := r.setting.Get(biz.SettingKeyMySQLRootPassword)
|
||||
rootPassword, err := NewSettingRepo().Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -274,21 +267,23 @@ func (r *backupRepo) createMySQL(to string, name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Compress([]string{backup}, backup+".zip", io.Zip); err != nil {
|
||||
if err = io.Compress(filepath.Dir(backup), []string{filepath.Base(backup)}, backup+".zip"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Remove(backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup+".zip"))
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
|
||||
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup+".zip"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createPostgres 创建 PostgreSQL 备份
|
||||
func (r *backupRepo) createPostgres(to string, name string) error {
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), 5432)
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", 5432)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -309,15 +304,17 @@ func (r *backupRepo) createPostgres(to string, name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Compress([]string{backup}, backup+".zip", io.Zip); err != nil {
|
||||
if err = io.Compress(filepath.Dir(backup), []string{filepath.Base(backup)}, backup+".zip"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Remove(backup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup+".zip"))
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
|
||||
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup+".zip"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -330,20 +327,36 @@ func (r *backupRepo) createPanel(to string) error {
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
if err := io.Compress([]string{
|
||||
filepath.Join(app.Root, "panel"),
|
||||
"/usr/local/sbin/panel-cli",
|
||||
"/usr/local/etc/panel/config.yml",
|
||||
}, backup, io.Zip); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := io.Chmod(backup, 0600); err != nil {
|
||||
|
||||
temp, err := io.TempDir("panel-backup")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-备份耗时:%s", time.Since(start).String()))
|
||||
color.Greenln(fmt.Sprintf("|-已备份至文件:%s", backup))
|
||||
return nil
|
||||
if err = io.Cp(filepath.Join(app.Root, "panel"), temp); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Cp("/usr/local/sbin/panel-cli", temp); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Cp("/usr/local/etc/panel/config.yml", temp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = io.Chmod(temp, 0600)
|
||||
if err = io.Compress(temp, nil, backup); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Chmod(backup, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-备份耗时:%s\n", time.Since(start).String())
|
||||
fmt.Printf("|-已备份至文件:%s\n", filepath.Base(backup))
|
||||
}
|
||||
|
||||
return io.Remove(temp)
|
||||
}
|
||||
|
||||
// restoreWebsite 恢复网站备份
|
||||
@@ -352,11 +365,7 @@ func (r *backupRepo) restoreWebsite(backup, target string) error {
|
||||
return errors.New("备份文件不存在")
|
||||
}
|
||||
|
||||
website, err := r.website.GetByName(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
format, err := io.FormatArchiveByPath(backup)
|
||||
website, err := NewWebsiteRepo().GetByName(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -364,7 +373,7 @@ func (r *backupRepo) restoreWebsite(backup, target string) error {
|
||||
if err = io.Remove(website.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.UnCompress(backup, website.Path, format); err != nil {
|
||||
if err = io.UnCompress(backup, website.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Chmod(website.Path, 0755); err != nil {
|
||||
@@ -383,11 +392,11 @@ func (r *backupRepo) restoreMySQL(backup, target string) error {
|
||||
return errors.New("备份文件不存在")
|
||||
}
|
||||
|
||||
rootPassword, err := r.setting.Get(biz.SettingKeyMySQLRootPassword)
|
||||
rootPassword, err := NewSettingRepo().Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -422,7 +431,7 @@ func (r *backupRepo) restorePostgres(backup, target string) error {
|
||||
return errors.New("备份文件不存在")
|
||||
}
|
||||
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), 5432)
|
||||
postgres, err := db.NewPostgres("postgres", "", "127.0.0.1", 5432)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -463,10 +472,12 @@ func (r *backupRepo) preCheckPath(to, path string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||
color.Greenln(fmt.Sprintf("|-目标文件数:%d", files))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-目标大小:%s\n", tools.FormatBytes(float64(size)))
|
||||
fmt.Printf("|-目标文件数:%d\n", files)
|
||||
fmt.Printf("|-备份目录可用空间:%s\n", tools.FormatBytes(float64(usage.Free)))
|
||||
fmt.Printf("|-备份目录可用Inode:%d\n", usage.InodesFree)
|
||||
}
|
||||
|
||||
if uint64(size) > usage.Free {
|
||||
return errors.New("备份目录空间不足")
|
||||
@@ -487,9 +498,11 @@ func (r *backupRepo) preCheckDB(to string, size int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln(fmt.Sprintf("|-目标大小:%s", str.FormatBytes(float64(size))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用空间:%s", str.FormatBytes(float64(usage.Free))))
|
||||
color.Greenln(fmt.Sprintf("|-备份目录可用Inode:%d", usage.InodesFree))
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-目标大小:%s\n", tools.FormatBytes(float64(size)))
|
||||
fmt.Printf("|-备份目录可用空间:%s\n", tools.FormatBytes(float64(usage.Free)))
|
||||
fmt.Printf("|-备份目录可用Inode:%d\n", usage.InodesFree)
|
||||
}
|
||||
|
||||
if uint64(size) > usage.Free {
|
||||
return errors.New("备份目录空间不足")
|
||||
@@ -505,11 +518,7 @@ func (r *backupRepo) autoUnCompressSQL(backup string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
format, err := io.FormatArchiveByPath(backup)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = io.UnCompress(backup, temp, format); err != nil {
|
||||
if err = io.UnCompress(backup, temp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/apploader"
|
||||
)
|
||||
|
||||
type cacheRepo struct{}
|
||||
type cacheRepo struct {
|
||||
api *api.API
|
||||
}
|
||||
|
||||
func NewCacheRepo() biz.CacheRepo {
|
||||
return &cacheRepo{}
|
||||
return do.MustInvoke[biz.CacheRepo](injector)
|
||||
}
|
||||
|
||||
func (r *cacheRepo) Get(key biz.CacheKey, defaultValue ...string) (string, error) {
|
||||
@@ -39,3 +46,37 @@ func (r *cacheRepo) Set(key biz.CacheKey, value string) error {
|
||||
cache.Value = value
|
||||
return app.Orm.Save(cache).Error
|
||||
}
|
||||
|
||||
func (r *cacheRepo) UpdateApps() error {
|
||||
remote, err := r.api.Apps()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 去除本地不存在的应用
|
||||
*remote = slices.Clip(slices.DeleteFunc(*remote, func(app *api.App) bool {
|
||||
_, err = apploader.Get(app.Slug)
|
||||
return err != nil
|
||||
}))
|
||||
|
||||
encoded, err := json.Marshal(remote)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Set(biz.CacheKeyApps, string(encoded))
|
||||
}
|
||||
|
||||
func (r *cacheRepo) UpdateRewrites() error {
|
||||
rewrites, err := r.api.RewritesByType("nginx")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
encoded, err := json.Marshal(rewrites)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Set(biz.CacheKeyRewrites, string(encoded))
|
||||
}
|
||||
|
||||
@@ -4,34 +4,62 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/acme"
|
||||
pkgcert "github.com/TheTNB/panel/pkg/cert"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
)
|
||||
|
||||
type certRepo struct {
|
||||
client *acme.Client
|
||||
websiteRepo biz.WebsiteRepo
|
||||
client *acme.Client
|
||||
}
|
||||
|
||||
func NewCertRepo() biz.CertRepo {
|
||||
return &certRepo{
|
||||
websiteRepo: NewWebsiteRepo(),
|
||||
}
|
||||
return do.MustInvoke[biz.CertRepo](injector)
|
||||
}
|
||||
|
||||
func (r *certRepo) List(page, limit uint) ([]*biz.Cert, int64, error) {
|
||||
func (r *certRepo) List(page, limit uint) ([]*types.CertList, int64, error) {
|
||||
var certs []*biz.Cert
|
||||
var total int64
|
||||
err := app.Orm.Model(&biz.Cert{}).Preload("Website").Preload("Account").Preload("DNS").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&certs).Error
|
||||
return certs, total, err
|
||||
|
||||
list := make([]*types.CertList, 0)
|
||||
for cert := range slices.Values(certs) {
|
||||
item := &types.CertList{
|
||||
ID: cert.ID,
|
||||
AccountID: cert.AccountID,
|
||||
WebsiteID: cert.WebsiteID,
|
||||
DNSID: cert.DNSID,
|
||||
Type: cert.Type,
|
||||
Domains: cert.Domains,
|
||||
AutoRenew: cert.AutoRenew,
|
||||
Cert: cert.Cert,
|
||||
Key: cert.Key,
|
||||
CreatedAt: cert.CreatedAt,
|
||||
UpdatedAt: cert.UpdatedAt,
|
||||
}
|
||||
if decode, err := pkgcert.ParseCert(cert.Cert); err == nil {
|
||||
item.NotBefore = decode.NotBefore
|
||||
item.NotAfter = decode.NotAfter
|
||||
item.Issuer = decode.Issuer.CommonName
|
||||
item.OCSPServer = decode.OCSPServer
|
||||
item.DNSNames = decode.DNSNames
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
|
||||
return list, total, err
|
||||
}
|
||||
|
||||
func (r *certRepo) Get(id uint) (*biz.Cert, error) {
|
||||
@@ -40,6 +68,34 @@ func (r *certRepo) Get(id uint) (*biz.Cert, error) {
|
||||
return cert, err
|
||||
}
|
||||
|
||||
func (r *certRepo) GetByWebsite(WebsiteID uint) (*biz.Cert, error) {
|
||||
cert := new(biz.Cert)
|
||||
err := app.Orm.Model(&biz.Cert{}).Preload("Website").Preload("Account").Preload("DNS").Where("website_id = ?", WebsiteID).First(cert).Error
|
||||
return cert, err
|
||||
}
|
||||
|
||||
func (r *certRepo) Upload(req *request.CertUpload) (*biz.Cert, error) {
|
||||
info, err := pkgcert.ParseCert(req.Cert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse certificate: %v", err)
|
||||
}
|
||||
if _, err = pkgcert.ParseKey(req.Key); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse private key: %v", err)
|
||||
}
|
||||
|
||||
cert := &biz.Cert{
|
||||
Type: "upload",
|
||||
Domains: info.DNSNames,
|
||||
Cert: req.Cert,
|
||||
Key: req.Key,
|
||||
}
|
||||
if err = app.Orm.Create(cert).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) {
|
||||
cert := &biz.Cert{
|
||||
AccountID: req.AccountID,
|
||||
@@ -56,11 +112,19 @@ func (r *certRepo) Create(req *request.CertCreate) (*biz.Cert, error) {
|
||||
}
|
||||
|
||||
func (r *certRepo) Update(req *request.CertUpdate) error {
|
||||
return app.Orm.Model(&biz.Cert{}).Where("id = ?", req.ID).Updates(&biz.Cert{
|
||||
info, err := pkgcert.ParseCert(req.Cert)
|
||||
if err == nil && req.Type == "upload" {
|
||||
req.Domains = info.DNSNames
|
||||
}
|
||||
|
||||
return app.Orm.Model(&biz.Cert{}).Where("id = ?", req.ID).Select("*").Updates(&biz.Cert{
|
||||
ID: req.ID,
|
||||
AccountID: req.AccountID,
|
||||
WebsiteID: req.WebsiteID,
|
||||
DNSID: req.DNSID,
|
||||
Type: req.Type,
|
||||
Cert: req.Cert,
|
||||
Key: req.Key,
|
||||
Domains: req.Domains,
|
||||
AutoRenew: req.AutoRenew,
|
||||
}).Error
|
||||
@@ -85,19 +149,19 @@ func (r *certRepo) ObtainAuto(id uint) (*acme.Certificate, error) {
|
||||
client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data)
|
||||
} else {
|
||||
if cert.Website == nil {
|
||||
return nil, errors.New("该证书没有关联网站,无法自动签发")
|
||||
return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually")
|
||||
} else {
|
||||
for _, domain := range cert.Domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
return nil, errors.New("通配符域名无法使用 HTTP 验证")
|
||||
return nil, errors.New("wildcard domains cannot use HTTP verification")
|
||||
}
|
||||
}
|
||||
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, cert.Website.Path)
|
||||
client.UseHTTP(conf)
|
||||
}
|
||||
}
|
||||
|
||||
ssl, err := client.ObtainSSL(context.Background(), cert.Domains, acme.KeyType(cert.Type))
|
||||
ssl, err := client.ObtainCertificate(context.Background(), cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -123,10 +187,10 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
|
||||
if r.client == nil {
|
||||
return nil, errors.New("请重新获取 DNS 解析记录")
|
||||
return nil, errors.New("please retry the manual obtain operation")
|
||||
}
|
||||
|
||||
ssl, err := r.client.ObtainSSLManual()
|
||||
ssl, err := r.client.ObtainCertificateManual()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -145,6 +209,30 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
|
||||
return &ssl, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) ObtainSelfSigned(id uint) error {
|
||||
cert, err := r.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
crt, key, err := pkgcert.GenerateSelfSigned(cert.Domains)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert.Cert = string(crt)
|
||||
cert.Key = string(key)
|
||||
if err = app.Orm.Save(cert).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
return r.Deploy(cert.ID, cert.WebsiteID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
|
||||
cert, err := r.Get(id)
|
||||
if err != nil {
|
||||
@@ -157,26 +245,26 @@ func (r *certRepo) Renew(id uint) (*acme.Certificate, error) {
|
||||
}
|
||||
|
||||
if cert.CertURL == "" {
|
||||
return nil, errors.New("该证书没有签发成功,无法续签")
|
||||
return nil, errors.New("this certificate has not been signed successfully and cannot be renewed")
|
||||
}
|
||||
|
||||
if cert.DNS != nil {
|
||||
client.UseDns(acme.DnsType(cert.DNS.Type), cert.DNS.Data)
|
||||
} else {
|
||||
if cert.Website == nil {
|
||||
return nil, errors.New("该证书没有关联网站,无法续签,可以尝试手动签发")
|
||||
return nil, errors.New("this certificate is not associated with a website and cannot be signed. You can try to sign it manually")
|
||||
} else {
|
||||
for _, domain := range cert.Domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
return nil, errors.New("通配符域名无法使用 HTTP 验证")
|
||||
return nil, errors.New("wildcard domains cannot use HTTP verification")
|
||||
}
|
||||
}
|
||||
conf := fmt.Sprintf("%s/server/vhost/acme/%s.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, cert.Website.Path)
|
||||
client.UseHTTP(conf)
|
||||
}
|
||||
}
|
||||
|
||||
ssl, err := client.RenewSSL(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
|
||||
ssl, err := client.RenewCertificate(context.Background(), cert.CertURL, cert.Domains, acme.KeyType(cert.Type))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -228,18 +316,18 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
}
|
||||
|
||||
if cert.Cert == "" || cert.Key == "" {
|
||||
return errors.New("该证书没有签发成功,无法部署")
|
||||
return errors.New("this certificate has not been signed successfully and cannot be deployed")
|
||||
}
|
||||
|
||||
website, err := r.websiteRepo.Get(WebsiteID)
|
||||
website, err := NewWebsiteRepo().Get(WebsiteID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.pem", app.Root, website.Name), cert.Cert, 0644); err != nil {
|
||||
if err = io.Write(fmt.Sprintf("%s/server/vhost/cert/%s.pem", app.Root, website.Name), cert.Cert, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = io.Write(fmt.Sprintf("%s/server/vhost/ssl/%s.key", app.Root, website.Name), cert.Key, 0644); err != nil {
|
||||
if err = io.Write(fmt.Sprintf("%s/server/vhost/cert/%s.key", app.Root, website.Name), cert.Key, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
@@ -251,6 +339,10 @@ func (r *certRepo) Deploy(ID, WebsiteID uint) error {
|
||||
}
|
||||
|
||||
func (r *certRepo) getClient(cert *biz.Cert) (*acme.Client, error) {
|
||||
if cert.Account == nil {
|
||||
return nil, errors.New("this certificate is not associated with an ACME account and cannot be signed")
|
||||
}
|
||||
|
||||
var ca string
|
||||
var eab *acme.EAB
|
||||
switch cert.Account.CA {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
type certAccountRepo struct{}
|
||||
|
||||
func NewCertAccountRepo() biz.CertAccountRepo {
|
||||
return &certAccountRepo{}
|
||||
return do.MustInvoke[biz.CertAccountRepo](injector)
|
||||
}
|
||||
|
||||
func (r certAccountRepo) List(page, limit uint) ([]*biz.CertAccount, int64, error) {
|
||||
@@ -28,6 +29,26 @@ func (r certAccountRepo) List(page, limit uint) ([]*biz.CertAccount, int64, erro
|
||||
return accounts, total, err
|
||||
}
|
||||
|
||||
func (r certAccountRepo) GetDefault(userID uint) (*biz.CertAccount, error) {
|
||||
user, err := NewUserRepo().Get(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
account := new(biz.CertAccount)
|
||||
if err = app.Orm.Model(&biz.CertAccount{}).Where("ca = ?", "googlecn").Where("email = ?", user.Email).First(account).Error; err == nil {
|
||||
return account, nil
|
||||
}
|
||||
|
||||
req := &request.CertAccountCreate{
|
||||
CA: "googlecn",
|
||||
Email: user.Email,
|
||||
KeyType: string(acme.KeyEC256),
|
||||
}
|
||||
|
||||
return r.Create(req)
|
||||
}
|
||||
|
||||
func (r certAccountRepo) Get(id uint) (*biz.CertAccount, error) {
|
||||
account := new(biz.CertAccount)
|
||||
err := app.Orm.Model(&biz.CertAccount{}).Where("id = ?", id).First(account).Error
|
||||
@@ -70,16 +91,16 @@ func (r certAccountRepo) Create(req *request.CertAccountCreate) (*biz.CertAccoun
|
||||
case "sslcom":
|
||||
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
|
||||
default:
|
||||
return nil, errors.New("CA 提供商不支持")
|
||||
return nil, errors.New("unsupported CA")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("注册账号失败:%v", err)
|
||||
return nil, fmt.Errorf("failed to register account: %v", err)
|
||||
}
|
||||
|
||||
privateKey, err := cert.EncodeKey(client.Account.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, errors.New("获取私钥失败")
|
||||
return nil, errors.New("failed to get private key")
|
||||
}
|
||||
account.PrivateKey = string(privateKey)
|
||||
|
||||
@@ -129,16 +150,16 @@ func (r certAccountRepo) Update(req *request.CertAccountUpdate) error {
|
||||
case "sslcom":
|
||||
client, err = acme.NewRegisterAccount(context.Background(), account.Email, acme.CASSLcom, &acme.EAB{KeyID: account.Kid, MACKey: account.HmacEncoded}, acme.KeyType(account.KeyType))
|
||||
default:
|
||||
return errors.New("CA 提供商不支持")
|
||||
return errors.New("unsupported CA")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return errors.New("向 CA 注册账号失败,请检查参数是否正确")
|
||||
return errors.New("failed to register account")
|
||||
}
|
||||
|
||||
privateKey, err := cert.EncodeKey(client.Account.PrivateKey)
|
||||
if err != nil {
|
||||
return errors.New("获取私钥失败")
|
||||
return errors.New("failed to get private key")
|
||||
}
|
||||
account.PrivateKey = string(privateKey)
|
||||
|
||||
@@ -162,13 +183,13 @@ func (r certAccountRepo) getGoogleEAB() (*acme.EAB, error) {
|
||||
client.SetTimeout(5 * time.Second)
|
||||
client.SetRetryCount(2)
|
||||
|
||||
resp, err := client.R().SetResult(&data{}).Get("https://panel.haozi.net/api/acme/googleEAB")
|
||||
resp, err := client.R().SetResult(&data{}).Get("https://gts.rat.dev/eab")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
return &acme.EAB{}, errors.New("获取Google EAB失败")
|
||||
return &acme.EAB{}, fmt.Errorf("failed to get Google EAB: %v", err)
|
||||
}
|
||||
eab := resp.Result().(*data)
|
||||
if eab.Message != "success" {
|
||||
return &acme.EAB{}, errors.New("获取Google EAB失败")
|
||||
return &acme.EAB{}, fmt.Errorf("failed to get Google EAB: %s", eab.Message)
|
||||
}
|
||||
|
||||
return &acme.EAB{KeyID: eab.Data.KeyId, MACKey: eab.Data.MacKey}, nil
|
||||
@@ -189,11 +210,11 @@ func (r certAccountRepo) getZeroSSLEAB(email string) (*acme.EAB, error) {
|
||||
"email": email,
|
||||
}).SetResult(&data{}).Post("https://api.zerossl.com/acme/eab-credentials-email")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败")
|
||||
return &acme.EAB{}, fmt.Errorf("failed to get ZeroSSL EAB: %v", err)
|
||||
}
|
||||
eab := resp.Result().(*data)
|
||||
if !eab.Success {
|
||||
return &acme.EAB{}, errors.New("获取ZeroSSL EAB失败")
|
||||
return &acme.EAB{}, fmt.Errorf("failed to get ZeroSSL EAB")
|
||||
}
|
||||
|
||||
return &acme.EAB{KeyID: eab.EabKid, MACKey: eab.EabHmacKey}, nil
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
@@ -9,7 +11,7 @@ import (
|
||||
type certDNSRepo struct{}
|
||||
|
||||
func NewCertDNSRepo() biz.CertDNSRepo {
|
||||
return &certDNSRepo{}
|
||||
return do.MustInvoke[biz.CertDNSRepo](injector)
|
||||
}
|
||||
|
||||
func (r certDNSRepo) List(page, limit uint) ([]*biz.CertDNS, int64, error) {
|
||||
|
||||
@@ -1,235 +1,209 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
paneltypes "github.com/TheTNB/panel/pkg/types"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
"github.com/TheTNB/panel/pkg/types/docker/container"
|
||||
)
|
||||
|
||||
type containerRepo struct {
|
||||
client *client.Client
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewContainerRepo(sock ...string) biz.ContainerRepo {
|
||||
if len(sock) == 0 {
|
||||
sock = append(sock, "/run/podman/podman.sock")
|
||||
}
|
||||
cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation())
|
||||
return &containerRepo{
|
||||
client: cli,
|
||||
}
|
||||
func NewContainerRepo() biz.ContainerRepo {
|
||||
return do.MustInvoke[biz.ContainerRepo](injector)
|
||||
}
|
||||
|
||||
// ListAll 列出所有容器
|
||||
func (r *containerRepo) ListAll() ([]types.Container, error) {
|
||||
containers, err := r.client.ContainerList(context.Background(), container.ListOptions{
|
||||
All: true,
|
||||
})
|
||||
var resp []container.Container
|
||||
_, err := r.client.R().SetResult(&resp).SetQueryParam("all", "true").Get("/containers/json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var containers []types.Container
|
||||
for _, item := range resp {
|
||||
ports := make([]types.ContainerPort, 0)
|
||||
for _, port := range item.Ports {
|
||||
ports = append(ports, types.ContainerPort{
|
||||
ContainerStart: uint(port.PrivatePort),
|
||||
ContainerEnd: uint(port.PublicPort),
|
||||
HostStart: uint(port.PublicPort),
|
||||
HostEnd: uint(port.PublicPort),
|
||||
Protocol: port.Type,
|
||||
Host: port.IP,
|
||||
})
|
||||
}
|
||||
if len(item.Names) == 0 {
|
||||
item.Names = append(item.Names, "")
|
||||
}
|
||||
containers = append(containers, types.Container{
|
||||
ID: item.ID,
|
||||
Name: strings.TrimPrefix(item.Names[0], "/"), // https://github.com/moby/moby/issues/7519
|
||||
Image: item.Image,
|
||||
ImageID: item.ImageID,
|
||||
Command: item.Command,
|
||||
CreatedAt: time.Unix(item.Created, 0),
|
||||
State: item.State,
|
||||
Status: item.Status,
|
||||
Ports: ports,
|
||||
Labels: types.MapToKV(item.Labels),
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(containers, func(a types.Container, b types.Container) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
// ListByNames 根据名称列出容器
|
||||
func (r *containerRepo) ListByNames(names []string) ([]types.Container, error) {
|
||||
var options container.ListOptions
|
||||
options.All = true
|
||||
if len(names) > 0 {
|
||||
var array []filters.KeyValuePair
|
||||
for _, n := range names {
|
||||
array = append(array, filters.Arg("name", n))
|
||||
}
|
||||
options.Filters = filters.NewArgs(array...)
|
||||
}
|
||||
containers, err := r.client.ContainerList(context.Background(), options)
|
||||
// ListByName 根据名称搜索容器
|
||||
func (r *containerRepo) ListByName(names string) ([]types.Container, error) {
|
||||
containers, err := r.ListAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containers = slices.DeleteFunc(containers, func(item types.Container) bool {
|
||||
return !strings.Contains(item.Name, names)
|
||||
})
|
||||
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
// Create 创建容器
|
||||
func (r *containerRepo) Create(req *request.ContainerCreate) (string, error) {
|
||||
var hostConf container.HostConfig
|
||||
var networkConf network.NetworkingConfig
|
||||
|
||||
portMap := make(nat.PortMap)
|
||||
for _, port := range req.Ports {
|
||||
if port.ContainerStart-port.ContainerEnd != port.HostStart-port.HostEnd {
|
||||
return "", fmt.Errorf("容器端口和主机端口数量不匹配(容器: %d 主机: %d)", port.ContainerStart-port.ContainerEnd, port.HostStart-port.HostEnd)
|
||||
}
|
||||
if port.ContainerStart > port.ContainerEnd || port.HostStart > port.HostEnd || port.ContainerStart < 1 || port.HostStart < 1 {
|
||||
return "", fmt.Errorf("端口范围不正确")
|
||||
}
|
||||
|
||||
count := 0
|
||||
for host := port.HostStart; host <= port.HostEnd; host++ {
|
||||
bindItem := nat.PortBinding{HostPort: strconv.Itoa(host), HostIP: port.Host}
|
||||
portMap[nat.Port(fmt.Sprintf("%d/%s", port.ContainerStart+count, port.Protocol))] = []nat.PortBinding{bindItem}
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
exposed := make(nat.PortSet)
|
||||
for port := range portMap {
|
||||
exposed[port] = struct{}{}
|
||||
}
|
||||
|
||||
if req.Network != "" {
|
||||
switch req.Network {
|
||||
case "host", "none", "bridge":
|
||||
hostConf.NetworkMode = container.NetworkMode(req.Network)
|
||||
}
|
||||
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{req.Network: {}}
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("docker run -d --name %s", req.Name))
|
||||
if req.PublishAllPorts {
|
||||
sb.WriteString(" -P")
|
||||
} else {
|
||||
networkConf = network.NetworkingConfig{}
|
||||
for _, port := range req.Ports {
|
||||
sb.WriteString(" -p ")
|
||||
if port.Host != "" {
|
||||
sb.WriteString(fmt.Sprintf("%s:", port.Host))
|
||||
}
|
||||
if port.HostStart == port.HostEnd || port.ContainerStart == port.ContainerEnd {
|
||||
sb.WriteString(fmt.Sprintf("%d:%d/%s", port.HostStart, port.ContainerStart, port.Protocol))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("%d-%d:%d-%d/%s", port.HostStart, port.HostEnd, port.ContainerStart, port.ContainerEnd, port.Protocol))
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.Network != "" {
|
||||
sb.WriteString(fmt.Sprintf(" --network %s", req.Network))
|
||||
}
|
||||
for _, volume := range req.Volumes {
|
||||
sb.WriteString(fmt.Sprintf(" -v %s:%s:%s", volume.Host, volume.Container, volume.Mode))
|
||||
}
|
||||
for _, label := range req.Labels {
|
||||
sb.WriteString(fmt.Sprintf(" --label %s=%s", label.Key, label.Value))
|
||||
}
|
||||
for _, env := range req.Env {
|
||||
sb.WriteString(fmt.Sprintf(" -e %s=%s", env.Key, env.Value))
|
||||
}
|
||||
if len(req.Entrypoint) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" --entrypoint '%s'", strings.Join(req.Entrypoint, " ")))
|
||||
}
|
||||
if len(req.Command) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" '%s'", strings.Join(req.Command, " ")))
|
||||
}
|
||||
if req.RestartPolicy != "" {
|
||||
sb.WriteString(fmt.Sprintf(" --restart %s", req.RestartPolicy))
|
||||
}
|
||||
if req.AutoRemove {
|
||||
sb.WriteString(" --rm")
|
||||
}
|
||||
if req.Privileged {
|
||||
sb.WriteString(" --privileged")
|
||||
}
|
||||
if req.OpenStdin {
|
||||
sb.WriteString(" -i")
|
||||
}
|
||||
if req.Tty {
|
||||
sb.WriteString(" -t")
|
||||
}
|
||||
if req.CPUShares > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" --cpu-shares %d", req.CPUShares))
|
||||
}
|
||||
if req.CPUs > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" --cpus %d", req.CPUs))
|
||||
}
|
||||
if req.Memory > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" --memory %d", req.Memory))
|
||||
}
|
||||
|
||||
hostConf.Privileged = req.Privileged
|
||||
hostConf.AutoRemove = req.AutoRemove
|
||||
hostConf.CPUShares = req.CPUShares
|
||||
hostConf.PublishAllPorts = req.PublishAllPorts
|
||||
hostConf.RestartPolicy = container.RestartPolicy{Name: container.RestartPolicyMode(req.RestartPolicy)}
|
||||
if req.RestartPolicy == "on-failure" {
|
||||
hostConf.RestartPolicy.MaximumRetryCount = 5
|
||||
}
|
||||
hostConf.NanoCPUs = req.CPUs * 1000000000
|
||||
hostConf.Memory = req.Memory * 1024 * 1024
|
||||
hostConf.MemorySwap = 0
|
||||
hostConf.PortBindings = portMap
|
||||
hostConf.Binds = []string{}
|
||||
|
||||
volumes := make(map[string]struct{})
|
||||
for _, v := range req.Volumes {
|
||||
volumes[v.Container] = struct{}{}
|
||||
hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", v.Host, v.Container, v.Mode))
|
||||
}
|
||||
|
||||
resp, err := r.client.ContainerCreate(context.Background(), &container.Config{
|
||||
Image: req.Image,
|
||||
Env: paneltypes.KVToSlice(req.Env),
|
||||
Entrypoint: req.Entrypoint,
|
||||
Cmd: req.Command,
|
||||
Labels: paneltypes.KVToMap(req.Labels),
|
||||
ExposedPorts: exposed,
|
||||
OpenStdin: req.OpenStdin,
|
||||
Tty: req.Tty,
|
||||
Volumes: volumes,
|
||||
}, &hostConf, &networkConf, nil, req.Name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp.ID, err
|
||||
sb.WriteString(" %s bash")
|
||||
return shell.ExecfWithTTY(sb.String(), req.Image)
|
||||
}
|
||||
|
||||
// Remove 移除容器
|
||||
func (r *containerRepo) Remove(id string) error {
|
||||
return r.client.ContainerRemove(context.Background(), id, container.RemoveOptions{
|
||||
Force: true,
|
||||
})
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker rm -f %s", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Start 启动容器
|
||||
func (r *containerRepo) Start(id string) error {
|
||||
return r.client.ContainerStart(context.Background(), id, container.StartOptions{})
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker start %s", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Stop 停止容器
|
||||
func (r *containerRepo) Stop(id string) error {
|
||||
return r.client.ContainerStop(context.Background(), id, container.StopOptions{})
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker stop %s", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Restart 重启容器
|
||||
func (r *containerRepo) Restart(id string) error {
|
||||
return r.client.ContainerRestart(context.Background(), id, container.StopOptions{})
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker restart %s", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Pause 暂停容器
|
||||
func (r *containerRepo) Pause(id string) error {
|
||||
return r.client.ContainerPause(context.Background(), id)
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker pause %s", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Unpause 恢复容器
|
||||
func (r *containerRepo) Unpause(id string) error {
|
||||
return r.client.ContainerUnpause(context.Background(), id)
|
||||
}
|
||||
|
||||
// Inspect 查看容器
|
||||
func (r *containerRepo) Inspect(id string) (types.ContainerJSON, error) {
|
||||
return r.client.ContainerInspect(context.Background(), id)
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker unpause %s", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Kill 杀死容器
|
||||
func (r *containerRepo) Kill(id string) error {
|
||||
return r.client.ContainerKill(context.Background(), id, "KILL")
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker kill %s", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename 重命名容器
|
||||
func (r *containerRepo) Rename(id string, newName string) error {
|
||||
return r.client.ContainerRename(context.Background(), id, newName)
|
||||
}
|
||||
|
||||
// Stats 查看容器状态
|
||||
func (r *containerRepo) Stats(id string) (container.StatsResponseReader, error) {
|
||||
return r.client.ContainerStats(context.Background(), id, false)
|
||||
}
|
||||
|
||||
// Exist 判断容器是否存在
|
||||
func (r *containerRepo) Exist(name string) (bool, error) {
|
||||
var options container.ListOptions
|
||||
options.Filters = filters.NewArgs(filters.Arg("name", name))
|
||||
containers, err := r.client.ContainerList(context.Background(), options)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return len(containers) > 0, nil
|
||||
}
|
||||
|
||||
// Update 更新容器
|
||||
func (r *containerRepo) Update(id string, config container.UpdateConfig) error {
|
||||
_, err := r.client.ContainerUpdate(context.Background(), id, config)
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker rename %s %s", id, newName)
|
||||
return err
|
||||
}
|
||||
|
||||
// Logs 查看容器日志
|
||||
func (r *containerRepo) Logs(id string) (string, error) {
|
||||
options := container.LogsOptions{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
}
|
||||
reader, err := r.client.ContainerLogs(context.Background(), id, options)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
return shell.ExecfWithTimeout(120*time.Second, "docker logs %s", id)
|
||||
}
|
||||
|
||||
// Prune 清理未使用的容器
|
||||
func (r *containerRepo) Prune() error {
|
||||
_, err := r.client.ContainersPrune(context.Background(), filters.NewArgs())
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker container prune -f")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,97 +1,87 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
"github.com/TheTNB/panel/pkg/types/docker/image"
|
||||
)
|
||||
|
||||
type containerImageRepo struct {
|
||||
client *client.Client
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewContainerImageRepo(sock ...string) biz.ContainerImageRepo {
|
||||
if len(sock) == 0 {
|
||||
sock = append(sock, "/run/podman/podman.sock")
|
||||
}
|
||||
cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation())
|
||||
return &containerImageRepo{
|
||||
client: cli,
|
||||
}
|
||||
func NewContainerImageRepo() biz.ContainerImageRepo {
|
||||
return do.MustInvoke[biz.ContainerImageRepo](injector)
|
||||
}
|
||||
|
||||
// List 列出镜像
|
||||
func (r *containerImageRepo) List() ([]image.Summary, error) {
|
||||
return r.client.ImageList(context.Background(), image.ListOptions{
|
||||
All: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Exist 判断镜像是否存在
|
||||
func (r *containerImageRepo) Exist(id string) (bool, error) {
|
||||
var options image.ListOptions
|
||||
options.Filters = filters.NewArgs(filters.Arg("reference", id))
|
||||
images, err := r.client.ImageList(context.Background(), options)
|
||||
func (r *containerImageRepo) List() ([]types.ContainerImage, error) {
|
||||
var resp []image.Image
|
||||
_, err := r.client.R().SetResult(&resp).SetQueryParam("all", "true").Get("/images/json")
|
||||
if err != nil {
|
||||
return false, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return len(images) > 0, nil
|
||||
var images []types.ContainerImage
|
||||
for _, item := range resp {
|
||||
images = append(images, types.ContainerImage{
|
||||
ID: item.ID,
|
||||
Containers: item.Containers,
|
||||
RepoTags: item.RepoTags,
|
||||
RepoDigests: item.RepoDigests,
|
||||
Size: tools.FormatBytes(float64(item.Size)),
|
||||
Labels: types.MapToKV(item.Labels),
|
||||
CreatedAt: time.Unix(item.Created, 0),
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(images, func(a types.ContainerImage, b types.ContainerImage) int {
|
||||
return strings.Compare(a.ID, b.ID)
|
||||
})
|
||||
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// Pull 拉取镜像
|
||||
func (r *containerImageRepo) Pull(req *request.ContainerImagePull) error {
|
||||
options := image.PullOptions{}
|
||||
var sb strings.Builder
|
||||
|
||||
if req.Auth {
|
||||
authConfig := registry.AuthConfig{
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
sb.WriteString(fmt.Sprintf("docker login -u %s -p %s", req.Username, req.Password))
|
||||
if _, err := shell.Exec(sb.String()); err != nil {
|
||||
return fmt.Errorf("login failed: %w", err)
|
||||
}
|
||||
encodedJSON, err := json.Marshal(authConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authStr := base64.URLEncoding.EncodeToString(encodedJSON)
|
||||
options.RegistryAuth = authStr
|
||||
sb.Reset()
|
||||
}
|
||||
|
||||
out, err := r.client.ImagePull(context.Background(), req.Name, options)
|
||||
if err != nil {
|
||||
sb.WriteString(fmt.Sprintf("docker pull %s", req.Name))
|
||||
|
||||
if _, err := shell.Exec(sb.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(io.Discard, out)
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove 删除镜像
|
||||
func (r *containerImageRepo) Remove(id string) error {
|
||||
_, err := r.client.ImageRemove(context.Background(), id, image.RemoveOptions{
|
||||
Force: true,
|
||||
PruneChildren: true,
|
||||
})
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker rmi %s", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Prune 清理未使用的镜像
|
||||
func (r *containerImageRepo) Prune() error {
|
||||
_, err := r.client.ImagesPrune(context.Background(), filters.NewArgs())
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker image prune -f")
|
||||
return err
|
||||
}
|
||||
|
||||
// Inspect 查看镜像
|
||||
func (r *containerImageRepo) Inspect(id string) (types.ImageInspect, error) {
|
||||
img, _, err := r.client.ImageInspectWithRaw(context.Background(), id)
|
||||
return img, err
|
||||
}
|
||||
|
||||
@@ -1,104 +1,114 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
"github.com/TheTNB/panel/pkg/types/docker/network"
|
||||
)
|
||||
|
||||
type containerNetworkRepo struct {
|
||||
client *client.Client
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewContainerNetworkRepo(sock ...string) biz.ContainerNetworkRepo {
|
||||
if len(sock) == 0 {
|
||||
sock = append(sock, "/run/podman/podman.sock")
|
||||
}
|
||||
cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation())
|
||||
return &containerNetworkRepo{
|
||||
client: cli,
|
||||
}
|
||||
func NewContainerNetworkRepo() biz.ContainerNetworkRepo {
|
||||
return do.MustInvoke[biz.ContainerNetworkRepo](injector)
|
||||
}
|
||||
|
||||
// List 列出网络
|
||||
func (r *containerNetworkRepo) List() ([]network.Inspect, error) {
|
||||
return r.client.NetworkList(context.Background(), network.ListOptions{})
|
||||
func (r *containerNetworkRepo) List() ([]types.ContainerNetwork, error) {
|
||||
var resp []network.Network
|
||||
_, err := r.client.R().SetResult(&resp).Get("/networks")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var networks []types.ContainerNetwork
|
||||
for _, item := range resp {
|
||||
ipamConfigs := make([]types.ContainerNetworkIPAMConfig, 0)
|
||||
for _, ipam := range item.IPAM.Config {
|
||||
ipamConfigs = append(ipamConfigs, types.ContainerNetworkIPAMConfig{
|
||||
Subnet: ipam.Subnet,
|
||||
IPRange: ipam.IPRange,
|
||||
Gateway: ipam.Gateway,
|
||||
AuxAddress: ipam.AuxAddress,
|
||||
})
|
||||
}
|
||||
networks = append(networks, types.ContainerNetwork{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
Driver: item.Driver,
|
||||
IPv6: item.EnableIPv6,
|
||||
Internal: item.Internal,
|
||||
Attachable: item.Attachable,
|
||||
Ingress: item.Ingress,
|
||||
Scope: item.Scope,
|
||||
CreatedAt: item.Created,
|
||||
IPAM: types.ContainerNetworkIPAM{
|
||||
Driver: item.IPAM.Driver,
|
||||
Options: types.MapToKV(item.IPAM.Options),
|
||||
Config: ipamConfigs,
|
||||
},
|
||||
Options: types.MapToKV(item.Options),
|
||||
Labels: types.MapToKV(item.Labels),
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(networks, func(a types.ContainerNetwork, b types.ContainerNetwork) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return networks, nil
|
||||
}
|
||||
|
||||
// Create 创建网络
|
||||
func (r *containerNetworkRepo) Create(req *request.ContainerNetworkCreate) (string, error) {
|
||||
var ipamConfigs []network.IPAMConfig
|
||||
if req.Ipv4.Enabled {
|
||||
ipamConfigs = append(ipamConfigs, network.IPAMConfig{
|
||||
Subnet: req.Ipv4.Subnet,
|
||||
Gateway: req.Ipv4.Gateway,
|
||||
IPRange: req.Ipv4.IPRange,
|
||||
})
|
||||
}
|
||||
if req.Ipv6.Enabled {
|
||||
ipamConfigs = append(ipamConfigs, network.IPAMConfig{
|
||||
Subnet: req.Ipv6.Subnet,
|
||||
Gateway: req.Ipv6.Gateway,
|
||||
IPRange: req.Ipv6.IPRange,
|
||||
})
|
||||
}
|
||||
var sb strings.Builder
|
||||
|
||||
options := network.CreateOptions{
|
||||
EnableIPv6: &req.Ipv6.Enabled,
|
||||
Driver: req.Driver,
|
||||
Options: types.KVToMap(req.Options),
|
||||
Labels: types.KVToMap(req.Labels),
|
||||
}
|
||||
if len(ipamConfigs) > 0 {
|
||||
options.IPAM = &network.IPAM{
|
||||
Config: ipamConfigs,
|
||||
sb.WriteString(fmt.Sprintf("docker network create --driver %s", req.Driver))
|
||||
sb.WriteString(fmt.Sprintf(" %s", req.Name))
|
||||
|
||||
if req.Ipv4.Enabled {
|
||||
sb.WriteString(fmt.Sprintf(" --subnet %s", req.Ipv4.Subnet))
|
||||
sb.WriteString(fmt.Sprintf(" --gateway %s", req.Ipv4.Gateway))
|
||||
if req.Ipv4.IPRange != "" {
|
||||
sb.WriteString(fmt.Sprintf(" --ip-range %s", req.Ipv4.IPRange))
|
||||
}
|
||||
}
|
||||
if req.Ipv6.Enabled {
|
||||
sb.WriteString(fmt.Sprintf(" --subnet %s", req.Ipv6.Subnet))
|
||||
sb.WriteString(fmt.Sprintf(" --gateway %s", req.Ipv6.Gateway))
|
||||
if req.Ipv6.IPRange != "" {
|
||||
sb.WriteString(fmt.Sprintf(" --ip-range %s", req.Ipv6.IPRange))
|
||||
}
|
||||
}
|
||||
for _, label := range req.Labels {
|
||||
sb.WriteString(fmt.Sprintf(" --label %s=%s", label.Key, label.Value))
|
||||
}
|
||||
for _, option := range req.Options {
|
||||
sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value))
|
||||
}
|
||||
|
||||
resp, err := r.client.NetworkCreate(context.Background(), req.Name, options)
|
||||
return resp.ID, err
|
||||
return shell.Exec(sb.String())
|
||||
}
|
||||
|
||||
// Remove 删除网络
|
||||
func (r *containerNetworkRepo) Remove(id string) error {
|
||||
return r.client.NetworkRemove(context.Background(), id)
|
||||
}
|
||||
|
||||
// Exist 判断网络是否存在
|
||||
func (r *containerNetworkRepo) Exist(name string) (bool, error) {
|
||||
var options network.ListOptions
|
||||
options.Filters = filters.NewArgs(filters.Arg("name", name))
|
||||
networks, err := r.client.NetworkList(context.Background(), options)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return len(networks) > 0, nil
|
||||
}
|
||||
|
||||
// Inspect 查看网络
|
||||
func (r *containerNetworkRepo) Inspect(id string) (network.Inspect, error) {
|
||||
return r.client.NetworkInspect(context.Background(), id, network.InspectOptions{})
|
||||
}
|
||||
|
||||
// Connect 连接网络
|
||||
func (r *containerNetworkRepo) Connect(networkID string, containerID string) error {
|
||||
return r.client.NetworkConnect(context.Background(), networkID, containerID, nil)
|
||||
}
|
||||
|
||||
// Disconnect 断开网络
|
||||
func (r *containerNetworkRepo) Disconnect(networkID string, containerID string) error {
|
||||
return r.client.NetworkDisconnect(context.Background(), networkID, containerID, true)
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker network rm -f %s", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Prune 清理未使用的网络
|
||||
func (r *containerNetworkRepo) Prune() error {
|
||||
_, err := r.client.NetworksPrune(context.Background(), filters.NewArgs())
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker network prune -f")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,74 +1,88 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/volume"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
"github.com/TheTNB/panel/pkg/types/docker/volume"
|
||||
)
|
||||
|
||||
type containerVolumeRepo struct {
|
||||
client *client.Client
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewContainerVolumeRepo(sock ...string) biz.ContainerVolumeRepo {
|
||||
if len(sock) == 0 {
|
||||
sock = append(sock, "/run/podman/podman.sock")
|
||||
}
|
||||
cli, _ := client.NewClientWithOpts(client.WithHost("unix://"+sock[0]), client.WithAPIVersionNegotiation())
|
||||
return &containerVolumeRepo{
|
||||
client: cli,
|
||||
}
|
||||
func NewContainerVolumeRepo() biz.ContainerVolumeRepo {
|
||||
return do.MustInvoke[biz.ContainerVolumeRepo](injector)
|
||||
}
|
||||
|
||||
// List 列出存储卷
|
||||
func (r *containerVolumeRepo) List() ([]*volume.Volume, error) {
|
||||
volumes, err := r.client.VolumeList(context.Background(), volume.ListOptions{})
|
||||
func (r *containerVolumeRepo) List() ([]types.ContainerVolume, error) {
|
||||
var resp volume.ListResponse
|
||||
_, err := r.client.R().SetResult(&resp).Get("/volumes")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return volumes.Volumes, err
|
||||
|
||||
var volumes []types.ContainerVolume
|
||||
for _, item := range resp.Volumes {
|
||||
volumes = append(volumes, types.ContainerVolume{
|
||||
Name: item.Name,
|
||||
Driver: item.Driver,
|
||||
Scope: item.Scope,
|
||||
MountPoint: item.Mountpoint,
|
||||
CreatedAt: item.CreatedAt,
|
||||
Labels: types.MapToKV(item.Labels),
|
||||
Options: types.MapToKV(item.Options),
|
||||
RefCount: item.UsageData.RefCount,
|
||||
Size: tools.FormatBytes(float64(item.UsageData.Size)),
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(volumes, func(a types.ContainerVolume, b types.ContainerVolume) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return volumes, nil
|
||||
}
|
||||
|
||||
// Create 创建存储卷
|
||||
func (r *containerVolumeRepo) Create(req *request.ContainerVolumeCreate) (volume.Volume, error) {
|
||||
return r.client.VolumeCreate(context.Background(), volume.CreateOptions{
|
||||
Name: req.Name,
|
||||
Driver: req.Driver,
|
||||
DriverOpts: types.KVToMap(req.Options),
|
||||
Labels: types.KVToMap(req.Labels),
|
||||
})
|
||||
}
|
||||
func (r *containerVolumeRepo) Create(req *request.ContainerVolumeCreate) (string, error) {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("docker volume create")
|
||||
sb.WriteString(fmt.Sprintf(" %s", req.Name))
|
||||
|
||||
// Exist 判断存储卷是否存在
|
||||
func (r *containerVolumeRepo) Exist(id string) (bool, error) {
|
||||
var options volume.ListOptions
|
||||
options.Filters = filters.NewArgs(filters.Arg("name", id))
|
||||
volumes, err := r.client.VolumeList(context.Background(), options)
|
||||
if err != nil {
|
||||
return false, err
|
||||
if req.Driver != "" {
|
||||
sb.WriteString(fmt.Sprintf(" --driver %s", req.Driver))
|
||||
}
|
||||
for _, label := range req.Labels {
|
||||
sb.WriteString(fmt.Sprintf(" --label %s=%s", label.Key, label.Value))
|
||||
}
|
||||
|
||||
return len(volumes.Volumes) > 0, nil
|
||||
}
|
||||
for _, option := range req.Options {
|
||||
sb.WriteString(fmt.Sprintf(" --opt %s=%s", option.Key, option.Value))
|
||||
}
|
||||
|
||||
// Inspect 查看存储卷
|
||||
func (r *containerVolumeRepo) Inspect(id string) (volume.Volume, error) {
|
||||
return r.client.VolumeInspect(context.Background(), id)
|
||||
return shell.Exec(sb.String())
|
||||
}
|
||||
|
||||
// Remove 删除存储卷
|
||||
func (r *containerVolumeRepo) Remove(id string) error {
|
||||
return r.client.VolumeRemove(context.Background(), id, true)
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker volume rm -f %s", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// Prune 清理未使用的存储卷
|
||||
func (r *containerVolumeRepo) Prune() error {
|
||||
_, err := r.client.VolumesPrune(context.Background(), filters.NewArgs())
|
||||
_, err := shell.ExecfWithTimeout(120*time.Second, "docker volume prune -f")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,28 +4,25 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/str"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/str"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type cronRepo struct {
|
||||
settingRepo biz.SettingRepo
|
||||
}
|
||||
type cronRepo struct{}
|
||||
|
||||
func NewCronRepo() biz.CronRepo {
|
||||
return &cronRepo{
|
||||
settingRepo: NewSettingRepo(),
|
||||
}
|
||||
return do.MustInvoke[biz.CronRepo](injector)
|
||||
}
|
||||
|
||||
func (r *cronRepo) Count() (int64, error) {
|
||||
@@ -54,10 +51,6 @@ func (r *cronRepo) Get(id uint) (*biz.Cron, error) {
|
||||
}
|
||||
|
||||
func (r *cronRepo) Create(req *request.CronCreate) error {
|
||||
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) {
|
||||
return errors.New("时间格式错误")
|
||||
}
|
||||
|
||||
var script string
|
||||
if req.Type == "backup" {
|
||||
if req.BackupType == "website" {
|
||||
@@ -92,6 +85,9 @@ panel-cli cutoff website -n %s -p %s
|
||||
panel-cli cutoff clear -t website -f %s -s %d -p %s
|
||||
`, req.Target, req.BackupPath, req.Target, req.Save, req.BackupPath)
|
||||
}
|
||||
if req.Type == "shell" {
|
||||
script = req.Script
|
||||
}
|
||||
|
||||
shellDir := fmt.Sprintf("%s/server/cron/", app.Root)
|
||||
shellLogDir := fmt.Sprintf("%s/server/cron/logs/", app.Root)
|
||||
@@ -101,7 +97,7 @@ panel-cli cutoff clear -t website -f %s -s %d -p %s
|
||||
if !io.Exists(shellLogDir) {
|
||||
return errors.New("计划任务日志目录不存在")
|
||||
}
|
||||
shellFile := strconv.Itoa(int(time.Now().Unix())) + str.RandomString(16)
|
||||
shellFile := strconv.Itoa(int(time.Now().Unix())) + str.Random(16)
|
||||
if err := io.Write(filepath.Join(shellDir, shellFile+".sh"), script, 0700); err != nil {
|
||||
return errors.New(err.Error())
|
||||
}
|
||||
@@ -133,10 +129,6 @@ func (r *cronRepo) Update(req *request.CronUpdate) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) {
|
||||
return errors.New("时间格式错误")
|
||||
}
|
||||
|
||||
if !cron.Status {
|
||||
return errors.New("计划任务已禁用")
|
||||
}
|
||||
@@ -200,24 +192,6 @@ func (r *cronRepo) Status(id uint, status bool) error {
|
||||
return app.Orm.Save(cron).Error
|
||||
}
|
||||
|
||||
func (r *cronRepo) Log(id uint) (string, error) {
|
||||
cron, err := r.Get(id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !io.Exists(cron.Log) {
|
||||
return "", errors.New("日志文件不存在")
|
||||
}
|
||||
|
||||
log, err := shell.Execf("tail -n 1000 '%s'", cron.Log)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
// addToSystem 添加到系统
|
||||
func (r *cronRepo) addToSystem(cron *biz.Cron) error {
|
||||
if _, err := shell.Execf(`( crontab -l; echo "%s %s >> %s 2>&1" ) | sort - | uniq - | crontab -`, cron.Time, cron.Shell, cron.Log); err != nil {
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
)
|
||||
|
||||
type databaseRepo struct{}
|
||||
|
||||
func NewDatabaseRepo() biz.DatabaseRepo {
|
||||
return do.MustInvoke[biz.DatabaseRepo](injector)
|
||||
}
|
||||
|
||||
func (r databaseRepo) List(page, limit uint) ([]*biz.Database, int64, error) {
|
||||
var databaseServer []*biz.DatabaseServer
|
||||
if err := app.Orm.Model(&biz.DatabaseServer{}).Order("id desc").Find(&databaseServer).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
database := make([]*biz.Database, 0)
|
||||
for _, server := range databaseServer {
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err == nil {
|
||||
if databases, err := mysql.Databases(); err == nil {
|
||||
for item := range slices.Values(databases) {
|
||||
database = append(database, &biz.Database{
|
||||
Type: biz.DatabaseTypeMysql,
|
||||
Name: item.Name,
|
||||
Server: server.Name,
|
||||
ServerID: server.ID,
|
||||
Encoding: item.CharSet,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
|
||||
if err == nil {
|
||||
if databases, err := postgres.Databases(); err == nil {
|
||||
for item := range slices.Values(databases) {
|
||||
database = append(database, &biz.Database{
|
||||
Type: biz.DatabaseTypePostgresql,
|
||||
Name: item.Name,
|
||||
Server: server.Name,
|
||||
ServerID: server.ID,
|
||||
Encoding: item.Encoding,
|
||||
Comment: item.Comment,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return database[(page-1)*limit:], int64(len(database)), nil
|
||||
}
|
||||
|
||||
func (r databaseRepo) Create(req *request.DatabaseCreate) error {
|
||||
server, err := NewDatabaseServerRepo().Get(req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.CreateUser {
|
||||
if err = NewDatabaseUserRepo().Create(&request.DatabaseUserCreate{
|
||||
ServerID: req.ServerID,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Host: req.Host,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = mysql.DatabaseCreate(req.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Username != "" {
|
||||
if err = mysql.PrivilegesGrant(req.Username, req.Name, req.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.CreateUser {
|
||||
if err = NewDatabaseUserRepo().Create(&request.DatabaseUserCreate{
|
||||
ServerID: req.ServerID,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Host: req.Host,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = postgres.DatabaseCreate(req.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Username != "" {
|
||||
if err = postgres.PrivilegesGrant(req.Username, req.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = postgres.DatabaseComment(req.Name, req.Comment); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r databaseRepo) Delete(serverID uint, name string) error {
|
||||
server, err := NewDatabaseServerRepo().Get(serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mysql.DatabaseDrop(name)
|
||||
case biz.DatabaseTypePostgresql:
|
||||
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return postgres.DatabaseDrop(name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r databaseRepo) Comment(req *request.DatabaseComment) error {
|
||||
server, err := NewDatabaseServerRepo().Get(req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
return errors.New("mysql not support database comment")
|
||||
case biz.DatabaseTypePostgresql:
|
||||
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return postgres.DatabaseComment(req.Name, req.Comment)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
)
|
||||
|
||||
type databaseServerRepo struct{}
|
||||
|
||||
func NewDatabaseServerRepo() biz.DatabaseServerRepo {
|
||||
return do.MustInvoke[biz.DatabaseServerRepo](injector)
|
||||
}
|
||||
|
||||
func (r databaseServerRepo) Count() (int64, error) {
|
||||
var count int64
|
||||
if err := app.Orm.Model(&biz.DatabaseServer{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r databaseServerRepo) List(page, limit uint) ([]*biz.DatabaseServer, int64, error) {
|
||||
var databaseServer []*biz.DatabaseServer
|
||||
var total int64
|
||||
err := app.Orm.Model(&biz.DatabaseServer{}).Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&databaseServer).Error
|
||||
|
||||
for server := range slices.Values(databaseServer) {
|
||||
r.checkServer(server)
|
||||
}
|
||||
|
||||
return databaseServer, total, err
|
||||
}
|
||||
|
||||
func (r databaseServerRepo) Get(id uint) (*biz.DatabaseServer, error) {
|
||||
databaseServer := new(biz.DatabaseServer)
|
||||
if err := app.Orm.Where("id = ?", id).First(databaseServer).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.checkServer(databaseServer)
|
||||
|
||||
return databaseServer, nil
|
||||
}
|
||||
|
||||
func (r databaseServerRepo) GetByName(name string) (*biz.DatabaseServer, error) {
|
||||
databaseServer := new(biz.DatabaseServer)
|
||||
if err := app.Orm.Where("name = ?", name).First(databaseServer).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.checkServer(databaseServer)
|
||||
|
||||
return databaseServer, nil
|
||||
}
|
||||
|
||||
func (r databaseServerRepo) Create(req *request.DatabaseServerCreate) error {
|
||||
databaseServer := &biz.DatabaseServer{
|
||||
Name: req.Name,
|
||||
Type: biz.DatabaseType(req.Type),
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
if !r.checkServer(databaseServer) {
|
||||
return fmt.Errorf("check server connection failed")
|
||||
}
|
||||
|
||||
return app.Orm.Create(databaseServer).Error
|
||||
}
|
||||
|
||||
func (r databaseServerRepo) Update(req *request.DatabaseServerUpdate) error {
|
||||
server, err := r.Get(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server.Name = req.Name
|
||||
server.Host = req.Host
|
||||
server.Port = req.Port
|
||||
server.Username = req.Username
|
||||
server.Password = req.Password
|
||||
server.Remark = req.Remark
|
||||
|
||||
if !r.checkServer(server) {
|
||||
return fmt.Errorf("check server connection failed")
|
||||
}
|
||||
|
||||
return app.Orm.Save(server).Error
|
||||
}
|
||||
|
||||
func (r databaseServerRepo) UpdateRemark(req *request.DatabaseServerUpdateRemark) error {
|
||||
return app.Orm.Model(&biz.DatabaseServer{}).Where("id = ?", req.ID).Update("remark", req.Remark).Error
|
||||
}
|
||||
|
||||
func (r databaseServerRepo) Delete(id uint) error {
|
||||
// 删除服务器下的所有用户
|
||||
if err := NewDatabaseUserRepo().DeleteByServerID(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Orm.Where("id = ?", id).Delete(&biz.DatabaseServer{}).Error
|
||||
}
|
||||
|
||||
func (r databaseServerRepo) Sync(id uint) error {
|
||||
server, err := r.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
users := make([]*biz.DatabaseUser, 0)
|
||||
if err = app.Orm.Where("server_id = ?", id).Find(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allUsers, err := mysql.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for user := range slices.Values(allUsers) {
|
||||
if !slices.ContainsFunc(users, func(a *biz.DatabaseUser) bool {
|
||||
return a.Username == user.User && a.Host == user.Host
|
||||
}) && !slices.Contains([]string{"root", "mysql.sys", "mysql.session", "mysql.infoschema"}, user.User) {
|
||||
newUser := &biz.DatabaseUser{
|
||||
ServerID: id,
|
||||
Username: user.User,
|
||||
Host: user.Host,
|
||||
Remark: fmt.Sprintf("sync from server %s", server.Name),
|
||||
}
|
||||
if err = app.Orm.Create(newUser).Error; err != nil {
|
||||
app.Logger.Warn("sync database user failed", slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
allUsers, err := postgres.Users()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for user := range slices.Values(allUsers) {
|
||||
if !slices.ContainsFunc(users, func(a *biz.DatabaseUser) bool {
|
||||
return a.Username == user.Role
|
||||
}) && !slices.Contains([]string{"postgres"}, user.Role) {
|
||||
newUser := &biz.DatabaseUser{
|
||||
ServerID: id,
|
||||
Username: user.Role,
|
||||
Remark: fmt.Sprintf("sync from server %s", server.Name),
|
||||
}
|
||||
app.Orm.Create(newUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkServer 检查服务器连接
|
||||
func (r databaseServerRepo) checkServer(server *biz.DatabaseServer) bool {
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
_, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err == nil {
|
||||
server.Status = biz.DatabaseServerStatusValid
|
||||
return true
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
_, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
|
||||
if err == nil {
|
||||
server.Status = biz.DatabaseServerStatusValid
|
||||
return true
|
||||
}
|
||||
case biz.DatabaseTypeRedis:
|
||||
_, err := db.NewRedis(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err == nil {
|
||||
server.Status = biz.DatabaseServerStatusValid
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
server.Status = biz.DatabaseServerStatusInvalid
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/db"
|
||||
)
|
||||
|
||||
type databaseUserRepo struct{}
|
||||
|
||||
func NewDatabaseUserRepo() biz.DatabaseUserRepo {
|
||||
return do.MustInvoke[biz.DatabaseUserRepo](injector)
|
||||
}
|
||||
|
||||
func (r databaseUserRepo) Count() (int64, error) {
|
||||
var count int64
|
||||
if err := app.Orm.Model(&biz.DatabaseUser{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (r databaseUserRepo) List(page, limit uint) ([]*biz.DatabaseUser, int64, error) {
|
||||
var user []*biz.DatabaseUser
|
||||
var total int64
|
||||
err := app.Orm.Model(&biz.DatabaseUser{}).Preload("Server").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&user).Error
|
||||
|
||||
for u := range slices.Values(user) {
|
||||
r.fillUser(u)
|
||||
}
|
||||
|
||||
return user, total, err
|
||||
}
|
||||
|
||||
func (r databaseUserRepo) Get(id uint) (*biz.DatabaseUser, error) {
|
||||
user := new(biz.DatabaseUser)
|
||||
if err := app.Orm.Preload("Server").Where("id = ?", id).First(user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.fillUser(user)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (r databaseUserRepo) Create(req *request.DatabaseUserCreate) error {
|
||||
server, err := NewDatabaseServerRepo().Get(req.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user := new(biz.DatabaseUser)
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = mysql.UserCreate(req.Username, req.Password, req.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
for name := range slices.Values(req.Privileges) {
|
||||
if err = mysql.PrivilegesGrant(req.Username, name, req.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
user = &biz.DatabaseUser{
|
||||
ServerID: req.ServerID,
|
||||
Username: req.Username,
|
||||
Host: req.Host,
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = postgres.UserCreate(req.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
for name := range slices.Values(req.Privileges) {
|
||||
if err = postgres.PrivilegesGrant(req.Username, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
user = &biz.DatabaseUser{
|
||||
ServerID: req.ServerID,
|
||||
Username: req.Username,
|
||||
}
|
||||
}
|
||||
|
||||
if err = app.Orm.FirstOrInit(user, user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Password = req.Password
|
||||
user.Remark = req.Remark
|
||||
|
||||
return app.Orm.Save(user).Error
|
||||
}
|
||||
|
||||
func (r databaseUserRepo) Update(req *request.DatabaseUserUpdate) error {
|
||||
user, err := r.Get(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := NewDatabaseServerRepo().Get(user.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Password != "" {
|
||||
if err = mysql.UserPassword(user.Username, req.Password, user.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for name := range slices.Values(req.Privileges) {
|
||||
if err = mysql.PrivilegesGrant(user.Username, name, user.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Password != "" {
|
||||
if err = postgres.UserPassword(user.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for name := range slices.Values(req.Privileges) {
|
||||
if err = postgres.PrivilegesGrant(user.Username, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user.Password = req.Password
|
||||
user.Remark = req.Remark
|
||||
|
||||
return app.Orm.Save(user).Error
|
||||
}
|
||||
|
||||
func (r databaseUserRepo) UpdateRemark(req *request.DatabaseUserUpdateRemark) error {
|
||||
user, err := r.Get(req.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user.Remark = req.Remark
|
||||
|
||||
return app.Orm.Save(user).Error
|
||||
}
|
||||
|
||||
func (r databaseUserRepo) Delete(id uint) error {
|
||||
user, err := r.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
server, err := NewDatabaseServerRepo().Get(user.ServerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = mysql.UserDrop(user.Username, user.Host)
|
||||
case biz.DatabaseTypePostgresql:
|
||||
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = postgres.UserDrop(user.Username)
|
||||
}
|
||||
|
||||
return app.Orm.Where("id = ?", id).Delete(&biz.DatabaseUser{}).Error
|
||||
}
|
||||
|
||||
func (r databaseUserRepo) DeleteByNames(serverID uint, names []string) error {
|
||||
server, err := NewDatabaseServerRepo().Get(serverID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
users := make([]*biz.DatabaseUser, 0)
|
||||
if err = app.Orm.Where("server_id = ? AND username IN ?", serverID, names).Find(&users).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for name := range slices.Values(names) {
|
||||
host := "localhost"
|
||||
for u := range slices.Values(users) {
|
||||
if u.Username == name {
|
||||
host = u.Host
|
||||
break
|
||||
}
|
||||
}
|
||||
_ = mysql.UserDrop(name, host)
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for name := range slices.Values(names) {
|
||||
_ = postgres.UserDrop(name)
|
||||
}
|
||||
}
|
||||
|
||||
return app.Orm.Where("server_id = ? AND username IN ?", serverID, names).Delete(&biz.DatabaseUser{}).Error
|
||||
}
|
||||
|
||||
// DeleteByServerID 删除指定服务器的所有用户,只是删除面板记录,不会实际删除
|
||||
func (r databaseUserRepo) DeleteByServerID(serverID uint) error {
|
||||
return app.Orm.Where("server_id = ?", serverID).Delete(&biz.DatabaseUser{}).Error
|
||||
}
|
||||
|
||||
func (r databaseUserRepo) fillUser(user *biz.DatabaseUser) {
|
||||
server, err := NewDatabaseServerRepo().Get(user.ServerID)
|
||||
if err == nil {
|
||||
switch server.Type {
|
||||
case biz.DatabaseTypeMysql:
|
||||
mysql, err := db.NewMySQL(server.Username, server.Password, fmt.Sprintf("%s:%d", server.Host, server.Port))
|
||||
if err == nil {
|
||||
privileges, _ := mysql.UserPrivileges(user.Username, user.Host)
|
||||
user.Privileges = privileges
|
||||
}
|
||||
if _, err := db.NewMySQL(user.Username, user.Password, fmt.Sprintf("%s:%d", server.Host, server.Port)); err == nil {
|
||||
user.Status = biz.DatabaseUserStatusValid
|
||||
} else {
|
||||
user.Status = biz.DatabaseUserStatusInvalid
|
||||
}
|
||||
case biz.DatabaseTypePostgresql:
|
||||
postgres, err := db.NewPostgres(server.Username, server.Password, server.Host, server.Port)
|
||||
if err == nil {
|
||||
privileges, _ := postgres.UserPrivileges(user.Username)
|
||||
user.Privileges = privileges
|
||||
}
|
||||
if _, err := db.NewPostgres(user.Username, user.Password, server.Host, server.Port); err == nil {
|
||||
user.Status = biz.DatabaseUserStatusValid
|
||||
} else {
|
||||
user.Status = biz.DatabaseUserStatusInvalid
|
||||
}
|
||||
}
|
||||
}
|
||||
// 初始化,防止 nil
|
||||
if user.Privileges == nil {
|
||||
user.Privileges = make([]string, 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-rat/utils/hash"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/pkg/api"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
)
|
||||
|
||||
var injector = do.New()
|
||||
|
||||
func init() {
|
||||
do.Provide(injector, func(i do.Injector) (biz.AppRepo, error) {
|
||||
return &appRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.BackupRepo, error) {
|
||||
return &backupRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.CacheRepo, error) {
|
||||
return &cacheRepo{
|
||||
api: api.NewAPI(app.Version),
|
||||
}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.CertRepo, error) {
|
||||
return &certRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.CertAccountRepo, error) {
|
||||
return &certAccountRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.CertDNSRepo, error) {
|
||||
return &certDNSRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.ContainerRepo, error) {
|
||||
return &containerRepo{
|
||||
client: getDockerClient("/var/run/docker.sock"),
|
||||
}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.ContainerImageRepo, error) {
|
||||
return &containerImageRepo{
|
||||
client: getDockerClient("/var/run/docker.sock"),
|
||||
}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.ContainerNetworkRepo, error) {
|
||||
return &containerNetworkRepo{
|
||||
client: getDockerClient("/var/run/docker.sock"),
|
||||
}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.ContainerVolumeRepo, error) {
|
||||
return &containerVolumeRepo{
|
||||
client: getDockerClient("/var/run/docker.sock"),
|
||||
}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.CronRepo, error) {
|
||||
return &cronRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.DatabaseServerRepo, error) {
|
||||
return &databaseServerRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.DatabaseUserRepo, error) {
|
||||
return &databaseUserRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.DatabaseRepo, error) {
|
||||
return &databaseRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.MonitorRepo, error) {
|
||||
return &monitorRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.SafeRepo, error) {
|
||||
var ssh string
|
||||
if os.IsRHEL() {
|
||||
ssh = "sshd"
|
||||
} else {
|
||||
ssh = "ssh"
|
||||
}
|
||||
return &safeRepo{
|
||||
ssh: ssh,
|
||||
}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.SettingRepo, error) {
|
||||
return &settingRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.SSHRepo, error) {
|
||||
return &sshRepo{}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.TaskRepo, error) {
|
||||
task := &taskRepo{}
|
||||
task.DispatchWaiting()
|
||||
return task, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.UserRepo, error) {
|
||||
return &userRepo{
|
||||
hasher: hash.NewArgon2id(),
|
||||
}, nil
|
||||
})
|
||||
do.Provide(injector, func(i do.Injector) (biz.WebsiteRepo, error) {
|
||||
return &websiteRepo{}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func getDockerClient(sock string) *resty.Client {
|
||||
client := resty.New()
|
||||
client.SetTimeout(1 * time.Minute)
|
||||
client.SetRetryCount(2)
|
||||
client.SetTransport(&http.Transport{
|
||||
DialContext: func(ctx context.Context, _ string, _ string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, "unix", sock)
|
||||
},
|
||||
})
|
||||
client.SetBaseURL("http://d/v1.40")
|
||||
return client
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
@@ -11,22 +12,19 @@ import (
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
)
|
||||
|
||||
type monitorRepo struct {
|
||||
settingRepo biz.SettingRepo
|
||||
}
|
||||
type monitorRepo struct{}
|
||||
|
||||
func NewMonitorRepo() biz.MonitorRepo {
|
||||
return &monitorRepo{
|
||||
settingRepo: NewSettingRepo(),
|
||||
}
|
||||
return do.MustInvoke[biz.MonitorRepo](injector)
|
||||
}
|
||||
|
||||
func (r monitorRepo) GetSetting() (*request.MonitorSetting, error) {
|
||||
monitor, err := r.settingRepo.Get(biz.SettingKeyMonitor)
|
||||
repo := NewSettingRepo()
|
||||
monitor, err := repo.Get(biz.SettingKeyMonitor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
monitorDays, err := r.settingRepo.Get(biz.SettingKeyMonitorDays)
|
||||
monitorDays, err := repo.Get(biz.SettingKeyMonitorDays)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -39,10 +37,11 @@ func (r monitorRepo) GetSetting() (*request.MonitorSetting, error) {
|
||||
}
|
||||
|
||||
func (r monitorRepo) UpdateSetting(setting *request.MonitorSetting) error {
|
||||
if err := r.settingRepo.Set(biz.SettingKeyMonitor, cast.ToString(setting.Enabled)); err != nil {
|
||||
repo := NewSettingRepo()
|
||||
if err := repo.Set(biz.SettingKeyMonitor, cast.ToString(setting.Enabled)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.settingRepo.Set(biz.SettingKeyMonitorDays, cast.ToString(setting.Days)); err != nil {
|
||||
if err := repo.Set(biz.SettingKeyMonitorDays, cast.ToString(setting.Days)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -50,7 +49,7 @@ func (r monitorRepo) UpdateSetting(setting *request.MonitorSetting) error {
|
||||
}
|
||||
|
||||
func (r monitorRepo) Clear() error {
|
||||
return app.Orm.Delete(&biz.Monitor{}).Error
|
||||
return app.Orm.Where("1 = 1").Delete(&biz.Monitor{}).Error
|
||||
}
|
||||
|
||||
func (r monitorRepo) List(start, end time.Time) ([]*biz.Monitor, error) {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/firewall"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/systemctl"
|
||||
)
|
||||
@@ -17,16 +18,7 @@ type safeRepo struct {
|
||||
}
|
||||
|
||||
func NewSafeRepo() biz.SafeRepo {
|
||||
var ssh string
|
||||
if os.IsRHEL() {
|
||||
ssh = "sshd"
|
||||
} else {
|
||||
ssh = "ssh"
|
||||
}
|
||||
|
||||
return &safeRepo{
|
||||
ssh: ssh,
|
||||
}
|
||||
return do.MustInvoke[biz.SafeRepo](injector)
|
||||
}
|
||||
|
||||
func (r *safeRepo) GetSSH() (uint, bool, error) {
|
||||
@@ -60,9 +52,9 @@ func (r *safeRepo) UpdateSSH(port uint, status bool) error {
|
||||
}
|
||||
|
||||
func (r *safeRepo) GetPingStatus() (bool, error) {
|
||||
out, err := shell.Execf(`firewall-cmd --list-all`)
|
||||
if err != nil {
|
||||
return true, errors.New(out)
|
||||
out, err := shell.Execf(`firewall-cmd --list-rich-rules`)
|
||||
if err != nil { // 可能防火墙已关闭等
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !strings.Contains(out, `rule protocol value="icmp" drop`) {
|
||||
@@ -73,7 +65,14 @@ func (r *safeRepo) GetPingStatus() (bool, error) {
|
||||
}
|
||||
|
||||
func (r *safeRepo) UpdatePingStatus(status bool) error {
|
||||
var err error
|
||||
fw, err := firewall.NewFirewall().Status()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fw {
|
||||
return fmt.Errorf("failed to update ping status: firewalld is not running")
|
||||
}
|
||||
|
||||
if status {
|
||||
_, err = shell.Execf(`firewall-cmd --permanent --remove-rich-rule='rule protocol value=icmp drop'`)
|
||||
} else {
|
||||
|
||||
@@ -8,15 +8,18 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/go-rat/utils/hash"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/gookit/color"
|
||||
"github.com/samber/do/v2"
|
||||
"github.com/spf13/cast"
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"github.com/TheTNB/panel/pkg/cert"
|
||||
"github.com/TheTNB/panel/pkg/firewall"
|
||||
"github.com/TheTNB/panel/pkg/io"
|
||||
"github.com/TheTNB/panel/pkg/os"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/tools"
|
||||
"github.com/TheTNB/panel/pkg/types"
|
||||
@@ -25,7 +28,7 @@ import (
|
||||
type settingRepo struct{}
|
||||
|
||||
func NewSettingRepo() biz.SettingRepo {
|
||||
return &settingRepo{}
|
||||
return do.MustInvoke[biz.SettingRepo](injector)
|
||||
}
|
||||
|
||||
func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, error) {
|
||||
@@ -43,6 +46,21 @@ func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, e
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
func (r *settingRepo) GetBool(key biz.SettingKey, defaultValue ...bool) (bool, error) {
|
||||
setting := new(biz.Setting)
|
||||
if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil {
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if setting.Value == "" && len(defaultValue) > 0 {
|
||||
return defaultValue[0], nil
|
||||
}
|
||||
|
||||
return cast.ToBool(setting.Value), nil
|
||||
}
|
||||
|
||||
func (r *settingRepo) Set(key biz.SettingKey, value string) error {
|
||||
setting := new(biz.Setting)
|
||||
if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil {
|
||||
@@ -66,16 +84,20 @@ func (r *settingRepo) Delete(key biz.SettingKey) error {
|
||||
}
|
||||
|
||||
func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSetting, error) {
|
||||
name := new(biz.Setting)
|
||||
if err := app.Orm.Where("key = ?", biz.SettingKeyName).First(name).Error; err != nil {
|
||||
name, err := r.Get(biz.SettingKeyName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
websitePath := new(biz.Setting)
|
||||
if err := app.Orm.Where("key = ?", biz.SettingKeyWebsitePath).First(websitePath).Error; err != nil {
|
||||
offlineMode, err := r.Get(biz.SettingKeyOfflineMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backupPath := new(biz.Setting)
|
||||
if err := app.Orm.Where("key = ?", biz.SettingKeyBackupPath).First(backupPath).Error; err != nil {
|
||||
websitePath, err := r.Get(biz.SettingKeyWebsitePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backupPath, err := r.Get(biz.SettingKeyBackupPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -85,7 +107,7 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
crt, err := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -95,16 +117,17 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
|
||||
}
|
||||
|
||||
return &request.PanelSetting{
|
||||
Name: name.Value,
|
||||
Name: name,
|
||||
Locale: app.Conf.String("app.locale"),
|
||||
Entrance: app.Conf.String("http.entrance"),
|
||||
WebsitePath: websitePath.Value,
|
||||
BackupPath: backupPath.Value,
|
||||
OfflineMode: cast.ToBool(offlineMode),
|
||||
WebsitePath: websitePath,
|
||||
BackupPath: backupPath,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Port: app.Conf.Int("http.port"),
|
||||
Port: uint(app.Conf.Int("http.port")),
|
||||
HTTPS: app.Conf.Bool("http.tls"),
|
||||
Cert: cert,
|
||||
Cert: crt,
|
||||
Key: key,
|
||||
}, nil
|
||||
}
|
||||
@@ -113,6 +136,9 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
||||
if err := r.Set(biz.SettingKeyName, setting.Name); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.Set(biz.SettingKeyOfflineMode, cast.ToString(setting.OfflineMode)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.Set(biz.SettingKeyWebsitePath, setting.WebsitePath); err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -120,43 +146,10 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), setting.Cert, 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), setting.Key, 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
restartFlag := false
|
||||
config := new(types.PanelConfig)
|
||||
cm := yaml.CommentMap{}
|
||||
raw, err := io.Read("/usr/local/etc/panel/config.yml")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err = yaml.UnmarshalWithOptions([]byte(raw), config, yaml.CommentToMap(cm)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
config.App.Locale = setting.Locale
|
||||
config.HTTP.Port = setting.Port
|
||||
config.HTTP.Entrance = setting.Entrance
|
||||
config.HTTP.TLS = setting.HTTPS
|
||||
|
||||
encoded, err := yaml.MarshalWithOptions(config, yaml.WithComment(cm))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if raw != string(encoded) {
|
||||
restartFlag = true
|
||||
}
|
||||
|
||||
// 用户
|
||||
user := new(biz.User)
|
||||
userID := cast.ToUint(ctx.Value("user_id"))
|
||||
if err = app.Orm.Where("id = ?", userID).First(user).Error; err != nil {
|
||||
if err := app.Orm.Where("id = ?", userID).First(user).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -169,7 +162,80 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
|
||||
}
|
||||
user.Password = value
|
||||
}
|
||||
if err = app.Orm.Save(user).Error; err != nil {
|
||||
if err := app.Orm.Save(user).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 下面是需要需要重启的设置
|
||||
task := NewTaskRepo()
|
||||
// 面板HTTPS
|
||||
restartFlag := false
|
||||
oldCert, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
oldKey, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.key"))
|
||||
if oldCert != setting.Cert || oldKey != setting.Key {
|
||||
if task.HasRunningTask() {
|
||||
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
|
||||
}
|
||||
restartFlag = true
|
||||
}
|
||||
if _, err := cert.ParseCert(setting.Cert); err != nil {
|
||||
return false, fmt.Errorf("failed to parse certificate: %w", err)
|
||||
}
|
||||
if _, err := cert.ParseKey(setting.Key); err != nil {
|
||||
return false, fmt.Errorf("failed to parse private key: %w", err)
|
||||
}
|
||||
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), setting.Cert, 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), setting.Key, 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// 面板主配置
|
||||
config := new(types.PanelConfig)
|
||||
raw, err := io.Read("/usr/local/etc/panel/config.yml")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err = yaml.Unmarshal([]byte(raw), config); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if setting.Port != config.HTTP.Port {
|
||||
if os.TCPPortInUse(setting.Port) {
|
||||
return false, errors.New("端口已被占用")
|
||||
}
|
||||
}
|
||||
|
||||
config.App.Locale = setting.Locale
|
||||
config.HTTP.Port = setting.Port
|
||||
config.HTTP.Entrance = setting.Entrance
|
||||
config.HTTP.TLS = setting.HTTPS
|
||||
|
||||
// 放行端口
|
||||
fw := firewall.NewFirewall()
|
||||
err = fw.Port(firewall.FireInfo{
|
||||
Type: firewall.TypeNormal,
|
||||
PortStart: uint(config.HTTP.Port),
|
||||
PortEnd: uint(config.HTTP.Port),
|
||||
Direction: firewall.DirectionIn,
|
||||
Strategy: firewall.StrategyAccept,
|
||||
}, firewall.OperationAdd)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
encoded, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if raw != string(encoded) {
|
||||
if task.HasRunningTask() {
|
||||
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
|
||||
}
|
||||
restartFlag = true
|
||||
}
|
||||
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0644); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -186,11 +252,15 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
|
||||
}
|
||||
|
||||
name := filepath.Base(url)
|
||||
color.Greenln(fmt.Sprintf("|-目标版本:%s", version))
|
||||
color.Greenln(fmt.Sprintf("|-下载链接:%s", url))
|
||||
color.Greenln(fmt.Sprintf("|-文件名:%s", name))
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-目标版本:%s\n", version)
|
||||
fmt.Printf("|-下载链接:%s\n", url)
|
||||
fmt.Printf("|-文件名:%s\n", name)
|
||||
}
|
||||
|
||||
color.Greenln("|-正在下载...")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-正在下载...")
|
||||
}
|
||||
if _, err := shell.Execf("wget -T 120 -t 3 -O /tmp/%s %s", name, url); err != nil {
|
||||
return fmt.Errorf("下载失败:%w", err)
|
||||
}
|
||||
@@ -201,59 +271,79 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
|
||||
return errors.New("下载文件检查失败")
|
||||
}
|
||||
|
||||
color.Greenln("|-校验下载文件...")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-校验下载文件...")
|
||||
}
|
||||
if check, err := shell.Execf("cd /tmp && sha256sum -c %s --ignore-missing", name+".sha256"); check != name+": OK" || err != nil {
|
||||
return errors.New("下载文件校验失败")
|
||||
}
|
||||
if err := io.Remove(filepath.Join("/tmp", name+".sha256")); err != nil {
|
||||
color.Redln("|-清理校验文件失败:", err)
|
||||
return err
|
||||
if app.IsCli {
|
||||
fmt.Println("|-清理校验文件失败:", err)
|
||||
}
|
||||
return fmt.Errorf("清理校验文件失败:%w", err)
|
||||
}
|
||||
|
||||
color.Greenln("|-前置检查...")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-前置检查...")
|
||||
}
|
||||
if io.Exists("/tmp/panel-storage.zip") {
|
||||
return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败所致,请运行 panel-cli fix 修复后重试")
|
||||
}
|
||||
|
||||
color.Greenln("|-备份面板数据...")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-备份面板数据...")
|
||||
}
|
||||
// 备份面板
|
||||
backup := NewBackupRepo()
|
||||
if err := backup.Create(biz.BackupTypePanel, ""); err != nil {
|
||||
color.Redln("|-备份面板失败:", err)
|
||||
return err
|
||||
if app.IsCli {
|
||||
fmt.Println("|-备份面板失败:", err)
|
||||
}
|
||||
return fmt.Errorf("备份面板失败:%w", err)
|
||||
}
|
||||
if err := io.Compress([]string{filepath.Join(app.Root, "panel/storage")}, "/tmp/panel-storage.zip", io.Zip); err != nil {
|
||||
color.Redln("|-备份面板数据失败:", err)
|
||||
return err
|
||||
if err := io.Compress(filepath.Join(app.Root, "panel/storage"), nil, "/tmp/panel-storage.zip"); err != nil {
|
||||
if app.IsCli {
|
||||
fmt.Println("|-备份面板数据失败:", err)
|
||||
}
|
||||
return fmt.Errorf("备份面板数据失败:%w", err)
|
||||
}
|
||||
if !io.Exists("/tmp/panel-storage.zip") {
|
||||
return errors.New("已备份面板数据检查失败")
|
||||
}
|
||||
|
||||
color.Greenln("|-清理旧版本...")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-清理旧版本...")
|
||||
}
|
||||
if _, err := shell.Execf("rm -rf %s/panel/*", app.Root); err != nil {
|
||||
return fmt.Errorf("清理旧版本失败:%w", err)
|
||||
}
|
||||
|
||||
color.Greenln("|-解压新版本...")
|
||||
if err := io.UnCompress(filepath.Join("/tmp", name), filepath.Join(app.Root, "panel"), io.Zip); err != nil {
|
||||
if app.IsCli {
|
||||
fmt.Println("|-解压新版本...")
|
||||
}
|
||||
if err := io.UnCompress(filepath.Join("/tmp", name), filepath.Join(app.Root, "panel")); err != nil {
|
||||
return fmt.Errorf("解压失败:%w", err)
|
||||
}
|
||||
if !io.Exists(filepath.Join(app.Root, "panel", "web")) {
|
||||
return errors.New("解压失败,缺失文件")
|
||||
}
|
||||
|
||||
color.Greenln("|-恢复面板数据...")
|
||||
if err := io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
|
||||
if app.IsCli {
|
||||
fmt.Println("|-恢复面板数据...")
|
||||
}
|
||||
if err := io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel", "storage")); err != nil {
|
||||
return fmt.Errorf("恢复面板数据失败:%w", err)
|
||||
}
|
||||
if !io.Exists(filepath.Join(app.Root, "panel/storage/app.db")) {
|
||||
return errors.New("恢复面板数据失败")
|
||||
}
|
||||
|
||||
color.Greenln("|-运行升级后脚本...")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-运行更新后脚本...")
|
||||
}
|
||||
if _, err := shell.Execf("curl -fsLm 10 https://dl.cdn.haozi.net/panel/auto_update.sh | bash"); err != nil {
|
||||
return fmt.Errorf("运行面板升级后脚本失败:%w", err)
|
||||
return fmt.Errorf("运行面板更新后脚本失败:%w", err)
|
||||
}
|
||||
if _, err := shell.Execf(`wget -O /etc/systemd/system/panel.service https://dl.cdn.haozi.net/panel/panel.service && sed -i "s|/www|%s|g" /etc/systemd/system/panel.service`, app.Root); err != nil {
|
||||
return fmt.Errorf("下载面板服务文件失败:%w", err)
|
||||
@@ -265,12 +355,16 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
|
||||
return fmt.Errorf("移动面板命令行工具失败:%w", err)
|
||||
}
|
||||
|
||||
color.Greenln("|-设置关键文件权限...")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-设置关键文件权限...")
|
||||
}
|
||||
_ = io.Chmod("/usr/local/sbin/panel-cli", 0700)
|
||||
_ = io.Chmod("/etc/systemd/system/panel.service", 0700)
|
||||
_ = io.Chmod(filepath.Join(app.Root, "panel"), 0700)
|
||||
|
||||
color.Greenln("|-升级完成")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-更新完成")
|
||||
}
|
||||
|
||||
_, _ = shell.Execf("systemctl daemon-reload")
|
||||
_ = io.Remove("/tmp/panel-storage.zip")
|
||||
@@ -281,7 +375,10 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
|
||||
}
|
||||
|
||||
func (r *settingRepo) FixPanel() error {
|
||||
color.Greenln("|-开始修复面板...")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-开始修复面板...")
|
||||
}
|
||||
|
||||
// 检查关键文件是否正常
|
||||
flag := false
|
||||
if !io.Exists(filepath.Join(app.Root, "panel", "web")) {
|
||||
@@ -294,7 +391,23 @@ func (r *settingRepo) FixPanel() error {
|
||||
flag = true
|
||||
}
|
||||
if !flag {
|
||||
return fmt.Errorf("文件正常无需修复,请运行 panel-cli update 升级版本")
|
||||
return fmt.Errorf("文件正常无需修复,请运行 panel-cli update 更新面板")
|
||||
}
|
||||
|
||||
// 再次确认是否需要修复
|
||||
if io.Exists("/tmp/panel-storage.zip") {
|
||||
// 文件齐全情况下只移除临时文件
|
||||
if io.Exists(filepath.Join(app.Root, "panel", "web")) &&
|
||||
io.Exists(filepath.Join(app.Root, "panel", "storage", "app.db")) &&
|
||||
io.Exists("/usr/local/etc/panel/config.yml") {
|
||||
if err := io.Remove("/tmp/panel-storage.zip"); err != nil {
|
||||
return fmt.Errorf("清理临时文件失败:%w", err)
|
||||
}
|
||||
if app.IsCli {
|
||||
fmt.Println("|-已清理临时文件,请运行 panel-cli update 更新面板")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// 从备份目录中找最新的备份文件
|
||||
@@ -310,19 +423,25 @@ func (r *settingRepo) FixPanel() error {
|
||||
return fmt.Errorf("未找到备份文件,无法自动修复")
|
||||
}
|
||||
latest := list[0]
|
||||
color.Greenln(fmt.Sprintf("|-使用备份文件:%s", latest.Name))
|
||||
if app.IsCli {
|
||||
fmt.Printf("|-使用备份文件:%s\n", latest.Name)
|
||||
}
|
||||
|
||||
// 解压备份文件
|
||||
color.Greenln("|-解压备份文件...")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-解压备份文件...")
|
||||
}
|
||||
if err = io.Remove("/tmp/panel-fix"); err != nil {
|
||||
return fmt.Errorf("清理临时目录失败:%w", err)
|
||||
}
|
||||
if err = io.UnCompress(latest.Path, "/tmp/panel-fix", io.Zip); err != nil {
|
||||
if err = io.UnCompress(latest.Path, "/tmp/panel-fix"); err != nil {
|
||||
return fmt.Errorf("解压备份文件失败:%w", err)
|
||||
}
|
||||
|
||||
// 移动文件到对应位置
|
||||
color.Greenln("|-移动备份文件...")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-移动备份文件...")
|
||||
}
|
||||
if io.Exists(filepath.Join("/tmp/panel-fix", "panel")) && io.IsDir(filepath.Join("/tmp/panel-fix", "panel")) {
|
||||
if err = io.Remove(filepath.Join(app.Root, "panel")); err != nil {
|
||||
return fmt.Errorf("删除目录失败:%w", err)
|
||||
@@ -342,10 +461,12 @@ func (r *settingRepo) FixPanel() error {
|
||||
}
|
||||
}
|
||||
|
||||
// tmp目录下如果有storage备份,则解压回去
|
||||
color.Greenln("|-恢复面板数据...")
|
||||
// tmp 目录下如果有 storage 备份,则解压回去
|
||||
if app.IsCli {
|
||||
fmt.Println("|-恢复面板数据...")
|
||||
}
|
||||
if io.Exists("/tmp/panel-storage.zip") {
|
||||
if err = io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel"), io.Zip); err != nil {
|
||||
if err = io.UnCompress("/tmp/panel-storage.zip", filepath.Join(app.Root, "panel")); err != nil {
|
||||
return fmt.Errorf("恢复面板数据失败:%w", err)
|
||||
}
|
||||
if err = io.Remove("/tmp/panel-storage.zip"); err != nil {
|
||||
@@ -361,7 +482,9 @@ func (r *settingRepo) FixPanel() error {
|
||||
}
|
||||
|
||||
// 处理权限
|
||||
color.Greenln("|-设置关键文件权限...")
|
||||
if app.IsCli {
|
||||
fmt.Println("|-设置关键文件权限...")
|
||||
}
|
||||
if err = io.Chmod("/usr/local/etc/panel/config.yml", 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -375,8 +498,10 @@ func (r *settingRepo) FixPanel() error {
|
||||
return err
|
||||
}
|
||||
|
||||
color.Greenln("|-修复完成")
|
||||
tools.RestartPanel()
|
||||
if app.IsCli {
|
||||
fmt.Println("|-修复完成")
|
||||
}
|
||||
|
||||
tools.RestartPanel()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,54 +1,87 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"github.com/samber/do/v2"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
pkgssh "github.com/TheTNB/panel/pkg/ssh"
|
||||
)
|
||||
|
||||
type sshRepo struct {
|
||||
settingRepo biz.SettingRepo
|
||||
}
|
||||
type sshRepo struct{}
|
||||
|
||||
func NewSSHRepo() biz.SSHRepo {
|
||||
return &sshRepo{
|
||||
settingRepo: NewSettingRepo(),
|
||||
}
|
||||
return do.MustInvoke[biz.SSHRepo](injector)
|
||||
}
|
||||
|
||||
func (r *sshRepo) GetInfo() (map[string]any, error) {
|
||||
host, _ := r.settingRepo.Get(biz.SettingKeySshHost)
|
||||
port, _ := r.settingRepo.Get(biz.SettingKeySshPort)
|
||||
user, _ := r.settingRepo.Get(biz.SettingKeySshUser)
|
||||
password, _ := r.settingRepo.Get(biz.SettingKeySshPassword)
|
||||
if len(host) == 0 || len(user) == 0 || len(password) == 0 {
|
||||
return nil, errors.New("SSH 配置不完整")
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"host": host,
|
||||
"port": cast.ToInt(port),
|
||||
"user": user,
|
||||
"password": password,
|
||||
}, nil
|
||||
func (r *sshRepo) List(page, limit uint) ([]*biz.SSH, int64, error) {
|
||||
var ssh []*biz.SSH
|
||||
var total int64
|
||||
err := app.Orm.Model(&biz.SSH{}).Omit("Hosts").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&ssh).Error
|
||||
return ssh, total, err
|
||||
}
|
||||
|
||||
func (r *sshRepo) UpdateInfo(req *request.SSHUpdateInfo) error {
|
||||
if err := r.settingRepo.Set(biz.SettingKeySshHost, req.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.settingRepo.Set(biz.SettingKeySshPort, cast.ToString(req.Port)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.settingRepo.Set(biz.SettingKeySshUser, req.User); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.settingRepo.Set(biz.SettingKeySshPassword, req.Password); err != nil {
|
||||
return err
|
||||
func (r *sshRepo) Get(id uint) (*biz.SSH, error) {
|
||||
ssh := new(biz.SSH)
|
||||
if err := app.Orm.Where("id = ?", id).First(ssh).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
return ssh, nil
|
||||
}
|
||||
|
||||
func (r *sshRepo) Create(req *request.SSHCreate) error {
|
||||
conf := pkgssh.ClientConfig{
|
||||
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
|
||||
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
|
||||
User: req.User,
|
||||
Password: req.Password,
|
||||
Key: req.Key,
|
||||
}
|
||||
_, err := pkgssh.NewSSHClient(conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check ssh connection: %v", err)
|
||||
}
|
||||
|
||||
ssh := &biz.SSH{
|
||||
Name: req.Name,
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Config: conf,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
return app.Orm.Create(ssh).Error
|
||||
}
|
||||
|
||||
func (r *sshRepo) Update(req *request.SSHUpdate) error {
|
||||
conf := pkgssh.ClientConfig{
|
||||
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
|
||||
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
|
||||
User: req.User,
|
||||
Password: req.Password,
|
||||
Key: req.Key,
|
||||
}
|
||||
_, err := pkgssh.NewSSHClient(conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check ssh connection: %v", err)
|
||||
}
|
||||
|
||||
ssh := &biz.SSH{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Config: conf,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
return app.Orm.Model(ssh).Where("id = ?", req.ID).Select("*").Updates(ssh).Error
|
||||
}
|
||||
|
||||
func (r *sshRepo) Delete(id uint) error {
|
||||
return app.Orm.Delete(&biz.SSH{}, id).Error
|
||||
}
|
||||
|
||||