mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 19:33:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e6bdc4f7 | ||
|
|
b31b42301e | ||
|
|
7ce0d876b0 | ||
|
|
2fd2596aa0 | ||
|
|
e0973e4a34 | ||
|
|
4f7b8a05ad | ||
|
|
2927d1ef2b | ||
|
|
3451f577ab | ||
|
|
06671be0d8 | ||
|
|
8ae9363f0f | ||
|
|
5d84445f8f | ||
|
|
7c541e4047 | ||
|
|
3262e5a11b | ||
|
|
ff43a373cd | ||
|
|
7394d948e4 | ||
|
|
5afb3aa43b | ||
|
|
7705af6e68 | ||
|
|
684e060ef3 | ||
|
|
0c67792441 | ||
|
|
b0ef2c2a87 | ||
|
|
1977c2a0fc | ||
|
|
d289708f9e | ||
|
|
cd5fd45667 | ||
|
|
50bc9ab129 | ||
|
|
bfad91dcab | ||
|
|
61a959adda | ||
|
|
f4b839fd11 | ||
|
|
969aeb5e0a | ||
|
|
68d42eb397 | ||
|
|
08603f9510 | ||
|
|
b450bf67bd | ||
|
|
03e26dbea8 | ||
|
|
046a3ac9f9 | ||
|
|
9c5aebaf36 | ||
|
|
a094715d69 | ||
|
|
69bb875172 | ||
|
|
589f10c7ef |
No files matched your search
+53
-25
@@ -43,21 +43,29 @@ template: |
|
|||||||
<td rowspan="2">🪟 Windows</td>
|
<td rowspan="2">🪟 Windows</td>
|
||||||
<td><code>EXE</code></td>
|
<td><code>EXE</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>Portable</code></td>
|
<td><code>Portable</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="4">🐧 Linux</td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
@@ -68,26 +76,46 @@ template: |
|
|||||||
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>DEB</code></td>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.deb">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.deb">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>AppImage</code></td>
|
<td>arm64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.deb">📥</a></td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-linux-x86_64.AppImage">📥</a></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td><code>APK</code></td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-android.apk">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-x86_64.apk">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
<td><code>IPA</code></td>
|
<td><code>IPA</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/Gopeed-v$NEXT_PATCH_VERSION-ios.ipa">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-ios.ipa">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🐳 Docker</td>
|
<td>🐳 Docker</td>
|
||||||
@@ -99,47 +127,47 @@ template: |
|
|||||||
<td rowspan="2">💾 Qnap</td>
|
<td rowspan="2">💾 Qnap</td>
|
||||||
<td rowspan="2"><code>QPKG</code></td>
|
<td rowspan="2"><code>QPKG</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-v$NEXT_PATCH_VERSION-qnap-amd64.qpkg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-amd64.qpkg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>arm64</td>
|
<td>arm64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-v$NEXT_PATCH_VERSION-qnap-arm64.qpkg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-arm64.qpkg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="8">🌐 Web</td>
|
<td rowspan="8">🌐 Web</td>
|
||||||
<td rowspan="3"><code>Windows</code></td>
|
<td rowspan="3"><code>Windows</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>arm64</td>
|
<td>arm64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-windows-arm64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-arm64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>386</td>
|
<td>386</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-windows-386.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-386.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="2"><code>MacOS</code></td>
|
<td rowspan="2"><code>MacOS</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-macos-amd64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-amd64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>arm64</td>
|
<td>arm64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-macos-arm64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-arm64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="3"><code>Linux</code></td>
|
<td rowspan="3"><code>Linux</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-linux-amd64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-amd64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>arm64</td>
|
<td>arm64</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-linux-arm64.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-arm64.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>386</td>
|
<td>386</td>
|
||||||
<td><a href="https://github.com/GopeedLab/gopeed/releases/download/v$NEXT_PATCH_VERSION/gopeed-web-v$NEXT_PATCH_VERSION-linux-386.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-386.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
+117
-43
@@ -13,7 +13,7 @@ on:
|
|||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.23"
|
GO_VERSION: "1.24"
|
||||||
FLUTTER_VERSION: "3.24.5"
|
FLUTTER_VERSION: "3.24.5"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -52,10 +52,11 @@ jobs:
|
|||||||
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
|
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
|
||||||
|
|
||||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$env:VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$env:VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
go build -ldflags="-w -s" -o ui/flutter/assets/host/host.exe github.com/GopeedLab/gopeed/cmd/host
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host.exe github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater.exe github.com/GopeedLab/gopeed/cmd/updater
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
$TAG = "v$env:VERSION"
|
$TAG = "v$env:VERSION"
|
||||||
flutter build windows
|
flutter build windows --dart-define="UPDATE_CHANNEL=windowsPortable"
|
||||||
$mingw = "C:\Program Files\Git\mingw64\bin"
|
$mingw = "C:\Program Files\Git\mingw64\bin"
|
||||||
$system = "C:\Windows\System32"
|
$system = "C:\Windows\System32"
|
||||||
$release = "build\windows\x64\runner\Release\"
|
$release = "build\windows\x64\runner\Release\"
|
||||||
@@ -69,6 +70,7 @@ jobs:
|
|||||||
New-Item -Path build\windows\Output -ItemType Directory
|
New-Item -Path build\windows\Output -ItemType Directory
|
||||||
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-amd64-portable.zip"
|
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-amd64-portable.zip"
|
||||||
|
|
||||||
|
flutter build windows --dart-define="UPDATE_CHANNEL=windowsInstaller"
|
||||||
cd build/windows
|
cd build/windows
|
||||||
echo @"
|
echo @"
|
||||||
; Script generated by the Inno Setup Script Wizard.
|
; Script generated by the Inno Setup Script Wizard.
|
||||||
@@ -137,9 +139,9 @@ jobs:
|
|||||||
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
asset_path: ui/flutter/build/windows/Output/*
|
asset_path: ui/flutter/build/windows/Output/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-macos-arm64-lib:
|
build-macos-amd64-lib:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||||
runs-on: macos-latest
|
runs-on: macos-13
|
||||||
needs: [get-release]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -152,16 +154,21 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/host
|
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/updater
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-amd64-lib
|
||||||
path: |
|
path: |
|
||||||
libgopeed.dylib
|
libgopeed.dylib
|
||||||
host
|
host
|
||||||
|
updater
|
||||||
build-macos:
|
build-macos:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||||
runs-on: macos-13
|
runs-on: macos-latest
|
||||||
needs: [get-release, build-macos-arm64-lib]
|
strategy:
|
||||||
|
matrix:
|
||||||
|
channel: [ arm64, amd64, universal ]
|
||||||
|
needs: [get-release, build-macos-amd64-lib]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -179,31 +186,47 @@ jobs:
|
|||||||
npm install -g appdmg
|
npm install -g appdmg
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-amd64-lib
|
||||||
path: ui/flutter/lib-arm64
|
path: ui/flutter/lib-amd64
|
||||||
|
- if: ${{ matrix.channel == 'arm64' }}
|
||||||
|
name: Build Arm64 Libraries
|
||||||
|
run: |
|
||||||
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
|
||||||
|
- if: ${{ matrix.channel == 'amd64' }}
|
||||||
|
name: Build Amd64 Libraries
|
||||||
|
run: |
|
||||||
|
mkdir -p ui/flutter/macos/Frameworks
|
||||||
|
|
||||||
|
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/libgopeed.dylib
|
||||||
|
cp ui/flutter/lib-amd64/host ui/flutter/assets/exec/host
|
||||||
|
cp ui/flutter/lib-amd64/updater ui/flutter/assets/exec/updater
|
||||||
|
- if: ${{ matrix.channel == 'universal' }}
|
||||||
|
name: Build Universal Libraries
|
||||||
|
run: |
|
||||||
|
mkdir -p ui/flutter/macos/Frameworks
|
||||||
|
|
||||||
|
# arm64 lib
|
||||||
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/.temp/libgopeed.dylib.arm64 github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/.temp/host.arm64 github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/.temp/updater.arm64 github.com/GopeedLab/gopeed/cmd/updater
|
||||||
|
|
||||||
|
# amd64 lib
|
||||||
|
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.amd64
|
||||||
|
cp ui/flutter/lib-amd64/host ui/flutter/.temp/host.amd64
|
||||||
|
cp ui/flutter/lib-amd64/updater ui/flutter/.temp/updater.amd64
|
||||||
|
|
||||||
|
# universal lib
|
||||||
|
lipo -create -output ui/flutter/macos/Frameworks/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.arm64 ui/flutter/.temp/libgopeed.dylib.amd64
|
||||||
|
lipo -create -output ui/flutter/assets/exec/host ui/flutter/.temp/host.arm64 ui/flutter/.temp/host.amd64
|
||||||
|
lipo -create -output ui/flutter/assets/exec/updater ui/flutter/.temp/updater.arm64 ui/flutter/.temp/updater.amd64
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
PROJECT_DIR=$(pwd)
|
cd ui/flutter
|
||||||
|
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
|
||||||
# amd64 lib
|
|
||||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/lib-amd64/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
|
||||||
go build -ldflags="-w -s" -o ui/flutter/lib-amd64/host github.com/GopeedLab/gopeed/cmd/host
|
|
||||||
# universal binary
|
|
||||||
mkdir -p ui/flutter/macos/Frameworks
|
|
||||||
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
|
|
||||||
cp ui/flutter/lib-arm64/libgopeed.dylib ui/flutter/macos/Frameworks/arm64-lib
|
|
||||||
cp ui/flutter/lib-amd64/host ui/flutter/macos/Frameworks/amd64-host
|
|
||||||
cp ui/flutter/lib-arm64/host ui/flutter/macos/Frameworks/arm64-host
|
|
||||||
cd ui/flutter/macos/Frameworks
|
|
||||||
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
|
|
||||||
lipo -create -output host amd64-host arm64-host
|
|
||||||
rm -rf amd64-lib arm64-lib amd64-host arm64-host
|
|
||||||
mv host ../../assets/host/host
|
|
||||||
|
|
||||||
cd $PROJECT_DIR/ui/flutter
|
|
||||||
flutter build macos
|
|
||||||
cd build/macos/Build/Products/Release
|
cd build/macos/Build/Products/Release
|
||||||
cat>appdmg.json<<EOF
|
cat>appdmg.json<<EOF
|
||||||
{
|
{
|
||||||
@@ -217,6 +240,13 @@ jobs:
|
|||||||
EOF
|
EOF
|
||||||
mkdir dist
|
mkdir dist
|
||||||
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
|
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
|
||||||
|
|
||||||
|
if [[ "${{ matrix.channel }}" == "arm64" ]]; then
|
||||||
|
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-arm64.dmg
|
||||||
|
fi
|
||||||
|
if [[ "${{ matrix.channel }}" == "amd64" ]]; then
|
||||||
|
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-amd64.dmg
|
||||||
|
fi
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -225,23 +255,34 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
build-linux:
|
build-linux:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-22.04, ubuntu-22.04-arm]
|
||||||
needs: [get-release]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- uses: subosito/flutter-action@v2
|
- if: matrix.os == 'ubuntu-22.04'
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- if: matrix.os == 'ubuntu-22.04-arm'
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
channel: master
|
||||||
- run: |
|
- run: |
|
||||||
sudo apt update -y
|
sudo apt update -y
|
||||||
# flutter build dependencies
|
# flutter build dependencies
|
||||||
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev
|
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev
|
||||||
# appimage build dependencies
|
# appimage build dependencies
|
||||||
sudo apt install -y libfuse2 locate
|
sudo apt install -y libfuse2 locate
|
||||||
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
arch=$(uname -m)
|
||||||
|
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
|
||||||
|
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
|
||||||
chmod +x appimagetool
|
chmod +x appimagetool
|
||||||
sudo mv appimagetool /usr/local/bin/
|
sudo mv appimagetool /usr/local/bin/
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -249,13 +290,42 @@ jobs:
|
|||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||||
go build -ldflags="-w -s" -o ui/flutter/assets/host/host github.com/GopeedLab/gopeed/cmd/host
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
|
||||||
|
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
|
cat << EOF > distribute_options.yaml
|
||||||
|
output: dist/
|
||||||
|
releases:
|
||||||
|
- name: linux
|
||||||
|
jobs:
|
||||||
|
- name: appimage
|
||||||
|
package:
|
||||||
|
platform: linux
|
||||||
|
target: appimage
|
||||||
|
build_args:
|
||||||
|
dart-define:
|
||||||
|
UPDATE_CHANNEL: linuxAppImage
|
||||||
|
- name: deb
|
||||||
|
package:
|
||||||
|
platform: linux
|
||||||
|
target: deb
|
||||||
|
build_args:
|
||||||
|
dart-define:
|
||||||
|
UPDATE_CHANNEL: linuxDeb
|
||||||
|
EOF
|
||||||
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
|
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
|
||||||
flutter_distributor package --platform linux --targets appimage,deb
|
flutter_distributor release --name linux
|
||||||
cd dist/*
|
cd dist/*
|
||||||
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-x86_64.AppImage
|
|
||||||
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-amd64.deb
|
# Get current running architecture, x86_64 -> amd64 and aarch64 -> arm64
|
||||||
|
ARCH="amd64"
|
||||||
|
if [[ "${{ matrix.os }}" == *-arm ]]; then
|
||||||
|
ARCH="arm64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-${ARCH}.AppImage
|
||||||
|
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-${ARCH}.deb
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -280,7 +350,8 @@ jobs:
|
|||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||||
go build -ldflags="-w -s" -o ui/flutter/assets/host/host github.com/GopeedLab/gopeed/cmd/host
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
|
|
||||||
sudo snap install snapcraft --classic
|
sudo snap install snapcraft --classic
|
||||||
@@ -357,7 +428,7 @@ jobs:
|
|||||||
set +e
|
set +e
|
||||||
dart pub get
|
dart pub get
|
||||||
set -eux
|
set -eux
|
||||||
flutter build linux --release -v
|
flutter build linux --dart-define="UPDATE_CHANNEL=linuxSnap"
|
||||||
mkdir -p \$CRAFT_PART_INSTALL/bin/
|
mkdir -p \$CRAFT_PART_INSTALL/bin/
|
||||||
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
|
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
|
||||||
gpu-2404:
|
gpu-2404:
|
||||||
@@ -422,9 +493,13 @@ jobs:
|
|||||||
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||||
flutter build apk
|
flutter build apk --dart-define="UPDATE_CHANNEL=androidApk"
|
||||||
|
flutter build apk --split-per-abi --dart-define="UPDATE_CHANNEL=androidApk"
|
||||||
mkdir dist
|
mkdir dist
|
||||||
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk
|
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk
|
||||||
|
cp build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk dist/Gopeed-v$VERSION-android-armeabi-v7a.apk
|
||||||
|
cp build/app/outputs/flutter-apk/app-arm64-v8a-release.apk dist/Gopeed-v$VERSION-android-arm64-v8a.apk
|
||||||
|
cp build/app/outputs/flutter-apk/app-x86_64-release.apk dist/Gopeed-v$VERSION-android-x86_64.apk
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
@@ -433,7 +508,7 @@ jobs:
|
|||||||
overwrite: true
|
overwrite: true
|
||||||
build-ios:
|
build-ios:
|
||||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
|
||||||
runs-on: macos-latest
|
runs-on: macos-14
|
||||||
needs: [get-release]
|
needs: [get-release]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -452,7 +527,7 @@ jobs:
|
|||||||
gomobile init
|
gomobile init
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build ios --no-codesign
|
flutter build ios --no-codesign --dart-define="UPDATE_CHANNEL=iosIpa"
|
||||||
mkdir Payload
|
mkdir Payload
|
||||||
cp -r build/ios/iphoneos/Runner.app Payload
|
cp -r build/ios/iphoneos/Runner.app Payload
|
||||||
zip -r -y Payload.zip Payload/Runner.app
|
zip -r -y Payload.zip Payload/Runner.app
|
||||||
@@ -480,7 +555,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.version }}
|
VERSION: ${{ needs.get-release.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
VERSION=${VERSION:-$(date +%y%m%d%H%M)} # Use current time as version if not specified
|
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web --no-web-resources-cdn
|
flutter build web --no-web-resources-cdn
|
||||||
cd ../../
|
cd ../../
|
||||||
@@ -598,7 +672,7 @@ jobs:
|
|||||||
- name: Build flutter web
|
- name: Build flutter web
|
||||||
run: |
|
run: |
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build web --no-web-resources-cdn
|
flutter build web --no-web-resources-cdn --dart-define="UPDATE_CHANNEL=docker"
|
||||||
cd ../../
|
cd ../../
|
||||||
rm -rf cmd/web/dist
|
rm -rf cmd/web/dist
|
||||||
cp -r ui/flutter/build/web cmd/web/dist
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ on:
|
|||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.23"
|
GO_VERSION: "1.24"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.23"
|
GO_VERSION: "1.24"
|
||||||
FLUTTER_VERSION: "3.24.5"
|
FLUTTER_VERSION: "3.24.5"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -53,7 +53,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
- name: Run Unit tests.
|
- name: Run Unit tests.
|
||||||
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
|
run: |
|
||||||
|
# Get packages for testing and coverage (including cmd/web for flags_test.go)
|
||||||
|
PACKAGES=$(go list ./... | grep -v /bind/ | grep -v -E '/cmd/(?!web)')
|
||||||
|
go test -v -coverpkg=$(echo "$PACKAGES" | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $PACKAGES
|
||||||
|
# Filter out main.go from coverage report to avoid lowering coverage percentage
|
||||||
|
grep -v "main.go" coverage.txt > coverage_filtered.txt || true
|
||||||
|
mv coverage_filtered.txt coverage.txt
|
||||||
- uses: codecov/codecov-action@v4
|
- uses: codecov/codecov-action@v4
|
||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
@@ -63,8 +69,8 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ windows-latest, macos-latest, ubuntu-22.04 ]
|
os: [windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm]
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -73,6 +79,7 @@ jobs:
|
|||||||
- uses: subosito/flutter-action@v2
|
- uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
channel: master
|
||||||
- if: runner.os == 'Windows'
|
- if: runner.os == 'Windows'
|
||||||
run: |
|
run: |
|
||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
@@ -95,8 +102,8 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos-latest, ubuntu-latest ]
|
os: [macos-14, ubuntu-latest]
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -128,7 +135,7 @@ jobs:
|
|||||||
build-web:
|
build-web:
|
||||||
name: Build web check
|
name: Build web check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -148,7 +155,7 @@ jobs:
|
|||||||
build-docker:
|
build-docker:
|
||||||
name: Build docker check
|
name: Build docker check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ test ]
|
needs: [test]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
|
|||||||
@@ -35,13 +35,21 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="4">🐧 Linux</td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
@@ -52,20 +60,40 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
|
|||||||
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>DEB</code></td>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>AppImage</code></td>
|
<td>arm64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">📥</a></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td><code>APK</code></td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
|
|||||||
+37
-10
@@ -35,13 +35,21 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="4">🐧 Linux</td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
@@ -52,20 +60,40 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
|||||||
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>DEB</code></td>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>AppImage</code></td>
|
<td>arm64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">📥</a></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td><code>APK</code></td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
@@ -127,7 +155,6 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
|
||||||
|
|
||||||
### 🛠️ コマンドツール
|
### 🛠️ コマンドツール
|
||||||
|
|||||||
+37
-10
@@ -35,13 +35,21 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="4">🐧 Linux</td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
@@ -52,20 +60,40 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
|
|||||||
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>DEB</code></td>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>AppImage</code></td>
|
<td>arm64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">📥</a></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td><code>APK</code></td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
@@ -127,7 +155,6 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html)
|
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html)
|
||||||
|
|
||||||
### 🛠️ Công cụ lệnh
|
### 🛠️ Công cụ lệnh
|
||||||
|
|||||||
+37
-10
@@ -35,13 +35,21 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="4">🐧 Linux</td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
@@ -52,20 +60,40 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>DEB</code></td>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>AppImage</code></td>
|
<td>arm64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">📥</a></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td><code>APK</code></td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
@@ -127,7 +155,6 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
|
||||||
|
|
||||||
### 🛠️ 命令行工具
|
### 🛠️ 命令行工具
|
||||||
|
|||||||
+37
-9
@@ -35,13 +35,21 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
|||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🍎 MacOS</td>
|
<td rowspan="3">🍎 MacOS</td>
|
||||||
<td><code>DMG</code></td>
|
<td rowspan="3"><code>DMG</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td rowspan="4">🐧 Linux</td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="6">🐧 Linux</td>
|
||||||
<td><code>Flathub</code></td>
|
<td><code>Flathub</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
|
||||||
@@ -52,20 +60,40 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
|||||||
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>DEB</code></td>
|
<td rowspan="2"><code>DEB</code></td>
|
||||||
<td>amd64</td>
|
<td>amd64</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>AppImage</code></td>
|
<td>arm64</td>
|
||||||
<td>amd64</td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">📥</a></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>🤖 Android</td>
|
<td rowspan="2"><code>AppImage</code></td>
|
||||||
<td><code>APK</code></td>
|
<td>amd64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td rowspan="4">🤖 Android</td>
|
||||||
|
<td rowspan="4"><code>APK</code></td>
|
||||||
<td>universal</td>
|
<td>universal</td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>armeabi-v7a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>arm64-v8a</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>x86_64</td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>📱 iOS</td>
|
<td>📱 iOS</td>
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ var apiMap = map[string]func(params json.RawMessage) (data any, err error){
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/host
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
for {
|
for {
|
||||||
// Read message length (first 4 bytes)
|
// Read message length (first 4 bytes)
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/browser"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/updater
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
pid := flag.Int("pid", 0, "PID of the process to update")
|
||||||
|
updateChannel := flag.String("channel", "", "Update channel")
|
||||||
|
packagePath := flag.String("asset", "", "Path to the package asset")
|
||||||
|
logPath := flag.String("log", "", "Log file path")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *pid == 0 {
|
||||||
|
log.Println("Invalid PID")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if *updateChannel == "" {
|
||||||
|
log.Println("Invalid update channel")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *logPath != "" {
|
||||||
|
logFile, err := os.OpenFile(*logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
|
||||||
|
if err == nil {
|
||||||
|
defer logFile.Close()
|
||||||
|
log.SetOutput(logFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
restart bool
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if restart, err = update(*pid, *updateChannel, *packagePath); err != nil {
|
||||||
|
log.Printf("Update failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart the application
|
||||||
|
if restart {
|
||||||
|
browser.OpenURL("gopeed:///")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete package asset
|
||||||
|
if *packagePath != "" {
|
||||||
|
os.Remove(*packagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(pid int, updateChannel, packagePath string) (restart bool, err error) {
|
||||||
|
killSignalChan := make(chan any, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-killSignalChan
|
||||||
|
|
||||||
|
if err = killProcess(pid); err != nil {
|
||||||
|
log.Printf("Failed to kill process: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = waitForProcessExit(pid); err != nil {
|
||||||
|
log.Printf("Failed to wait for process exit: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
appDir := filepath.Dir(os.Args[0])
|
||||||
|
log.Printf("Updating process updateChannel=%s packagePath=%s appDir=%s\n", updateChannel, packagePath, appDir)
|
||||||
|
if restart, err = install(killSignalChan, updateChannel, packagePath, appDir); err != nil {
|
||||||
|
return false, errors.Wrap(err, "failed to install package")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForProcessExit(pid int) error {
|
||||||
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
// On some systems, error is returned if process doesn't exist
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send null signal to test if process exists
|
||||||
|
err = process.Signal(syscall.Signal(0))
|
||||||
|
if err != nil {
|
||||||
|
// If error occurs, the process no longer exists
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("process %d still running after timeout", pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func killProcess(pid int) error {
|
||||||
|
process, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.Kill()
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
|
||||||
|
return true, installByDmg(killSignalChan, packagePath, destDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByDmg handles macOS dmg package installation
|
||||||
|
func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error {
|
||||||
|
// /Applications/Gopeed.app/Contents/MacOS -> /Applications
|
||||||
|
appPath := getParentDir(getParentDir(getParentDir(destDir)))
|
||||||
|
output, err := exec.Command("hdiutil", "attach", packagePath, "-nobrowse").Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mountPoint := ""
|
||||||
|
for _, line := range strings.Split(string(output), "\n") {
|
||||||
|
if strings.Contains(line, "/Volumes/") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) > 2 {
|
||||||
|
mountPoint = fields[len(fields)-1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if mountPoint == "" {
|
||||||
|
return fmt.Errorf("failed to get mount point")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detach the mounted DMG
|
||||||
|
defer exec.Command("hdiutil", "detach", mountPoint, "-quiet").Run()
|
||||||
|
|
||||||
|
matches, err := filepath.Glob(filepath.Join(mountPoint, "*.app"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return fmt.Errorf("no .app found in dmg")
|
||||||
|
}
|
||||||
|
|
||||||
|
killSignalChan <- nil
|
||||||
|
|
||||||
|
// Copy the new app to the destination
|
||||||
|
// cp -Rf /Volumes/GoPeed/GoPeed.app /Applications
|
||||||
|
if err := exec.Command("cp", "-Rf", matches[0], appPath).Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parent directory safely, handling trailing separators
|
||||||
|
func getParentDir(path string) string {
|
||||||
|
// Remove trailing separators if they exist
|
||||||
|
path = strings.TrimRight(path, string(filepath.Separator))
|
||||||
|
// Now get the parent directory
|
||||||
|
return filepath.Dir(path)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
|
||||||
|
killSignalChan <- nil
|
||||||
|
switch updateChannel {
|
||||||
|
case "linuxDeb":
|
||||||
|
return true, installByDeb(packagePath)
|
||||||
|
case "linuxFlathub":
|
||||||
|
return true, installByFlathub()
|
||||||
|
case "linuxSnap":
|
||||||
|
return true, installBySnap()
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("unsupported update channel for Linux: %s", updateChannel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeInTerminal tries to execute a command in one of several common terminal emulators
|
||||||
|
func executeInTerminal(command string) error {
|
||||||
|
terminals := []string{
|
||||||
|
"gnome-terminal", // GNOME
|
||||||
|
"konsole", // KDE
|
||||||
|
"xfce4-terminal", // XFCE
|
||||||
|
"xterm", // X11
|
||||||
|
}
|
||||||
|
|
||||||
|
command = fmt.Sprintf(`echo "Starting update..." && echo "[CMD] %s" && %s`, command, command)
|
||||||
|
|
||||||
|
for _, term := range terminals {
|
||||||
|
if _, err := exec.LookPath(term); err == nil {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch term {
|
||||||
|
case "gnome-terminal", "xfce4-terminal":
|
||||||
|
cmd = exec.Command(term, "--", "bash", "-c", command)
|
||||||
|
case "konsole":
|
||||||
|
cmd = exec.Command(term, "-e", "bash", "-c", command)
|
||||||
|
case "xterm":
|
||||||
|
cmd = exec.Command(term, "-e", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("no suitable terminal emulator found. Please install gnome-terminal, konsole, xfce4-terminal or xterm")
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByDeb installs the .deb package
|
||||||
|
func installByDeb(packagePath string) error {
|
||||||
|
command := fmt.Sprintf(`sudo dpkg -i "%s"`, packagePath)
|
||||||
|
return executeInTerminal(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByFlathub updates the application via Flathub
|
||||||
|
func installByFlathub() error {
|
||||||
|
command := "flatpak update com.gopeed.Gopeed -y"
|
||||||
|
return executeInTerminal(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// installBySnap updates the application via Snap
|
||||||
|
func installBySnap() error {
|
||||||
|
command := "sudo snap refresh gopeed"
|
||||||
|
return executeInTerminal(command)
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
|
||||||
|
switch updateChannel {
|
||||||
|
case "windowsInstaller":
|
||||||
|
return false, installByInstaller(killSignalChan, packagePath, destDir)
|
||||||
|
default:
|
||||||
|
return true, installByPortable(killSignalChan, packagePath, destDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByInstaller extracts the installer from the zip file and runs it
|
||||||
|
func installByInstaller(killSignalChan chan<- any, packagePath, destDir string) error {
|
||||||
|
// Create a temp directory for extraction
|
||||||
|
tempDir, err := os.MkdirTemp("", "gopeed_update")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
// Extract the zip file
|
||||||
|
reader, err := zip.OpenReader(packagePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
// Find the installer file
|
||||||
|
var installerPath string
|
||||||
|
for _, file := range reader.File {
|
||||||
|
if file.FileInfo().IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract file
|
||||||
|
path := filepath.Join(tempDir, file.Name)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srcFile, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
dstFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(dstFile, srcFile)
|
||||||
|
srcFile.Close()
|
||||||
|
dstFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is likely an installer (.exe, .msi), save its path
|
||||||
|
ext := strings.ToLower(filepath.Ext(file.Name))
|
||||||
|
if ext == ".exe" || ext == ".msi" {
|
||||||
|
installerPath = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if installerPath == "" {
|
||||||
|
return fmt.Errorf("no installer found in the update package")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the installer
|
||||||
|
cmd := exec.Command(installerPath)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
killSignalChan <- nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// installByPortable extracts the portable version to the destination directory
|
||||||
|
func installByPortable(killSignalChan chan<- any, packagePath, destDir string) error {
|
||||||
|
killSignalChan <- nil
|
||||||
|
|
||||||
|
reader, err := zip.OpenReader(packagePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
for _, file := range reader.File {
|
||||||
|
path := filepath.Join(destDir, file.Name)
|
||||||
|
|
||||||
|
if file.FileInfo().IsDir() {
|
||||||
|
os.MkdirAll(path, file.Mode())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srcFile, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
dstFile.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(dstFile, srcFile)
|
||||||
|
srcFile.Close()
|
||||||
|
dstFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+170
-51
@@ -3,19 +3,23 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
)
|
)
|
||||||
|
|
||||||
type args struct {
|
type args struct {
|
||||||
Address *string `json:"address"`
|
Address *string `json:"address"`
|
||||||
Port *int `json:"port"`
|
Port *int `json:"port"`
|
||||||
Username *string `json:"username"`
|
Username *string `json:"username"`
|
||||||
Password *string `json:"password"`
|
Password *string `json:"password"`
|
||||||
ApiToken *string `json:"apiToken"`
|
ApiToken *string `json:"apiToken"`
|
||||||
StorageDir *string `json:"storageDir"`
|
StorageDir *string `json:"storageDir"`
|
||||||
|
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
|
||||||
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
// DownloadConfig when the first time to start the server, it will be configured as initial value
|
||||||
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
|
||||||
|
|
||||||
@@ -23,62 +27,177 @@ type args struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parse() *args {
|
func parse() *args {
|
||||||
var cliArgs args
|
cfg := &args{}
|
||||||
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
|
|
||||||
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
|
cliConfig := loadCliArgs()
|
||||||
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
|
loadConfigFile(cfg, *cliConfig.configPath)
|
||||||
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
|
loadEnvVars(cfg)
|
||||||
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling basic authentication")
|
// override with non-default command line arguments
|
||||||
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
|
overrideWithCliArgs(cfg, cliConfig)
|
||||||
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
|
// set default values for any unset fields
|
||||||
|
setDefaults(cfg)
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadCliArgs parses command line arguments and returns initial config
|
||||||
|
func loadCliArgs() *args {
|
||||||
|
cfg := &args{}
|
||||||
|
cfg.Address = flag.String("A", "", "Bind Address")
|
||||||
|
cfg.Port = flag.Int("P", 0, "Bind Port")
|
||||||
|
cfg.Username = flag.String("u", "", "Web Authentication Username")
|
||||||
|
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
|
||||||
|
cfg.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
|
||||||
|
cfg.StorageDir = flag.String("d", "", "Storage directory")
|
||||||
|
whiteDownloadDirs := flag.String("w", "", "White download directories, comma-separated")
|
||||||
|
cfg.configPath = flag.String("c", "./config.json", "Config file path")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// args priority: config file > cli args
|
// Parse white download directories from comma-separated string
|
||||||
cfgArgs := loadConfig(*cliArgs.configPath)
|
if whiteDownloadDirs != nil && *whiteDownloadDirs != "" {
|
||||||
if cfgArgs.Address == nil {
|
dirs := strings.Split(*whiteDownloadDirs, ",")
|
||||||
cfgArgs.Address = cliArgs.Address
|
for i := range dirs {
|
||||||
|
dirs[i] = strings.TrimSpace(dirs[i])
|
||||||
|
}
|
||||||
|
cfg.WhiteDownloadDirs = dirs
|
||||||
}
|
}
|
||||||
if cfgArgs.Port == nil {
|
|
||||||
cfgArgs.Port = cliArgs.Port
|
return cfg
|
||||||
}
|
|
||||||
if cfgArgs.Username == nil {
|
|
||||||
cfgArgs.Username = cliArgs.Username
|
|
||||||
}
|
|
||||||
if cfgArgs.Password == nil {
|
|
||||||
cfgArgs.Password = cliArgs.Password
|
|
||||||
}
|
|
||||||
if cfgArgs.ApiToken == nil {
|
|
||||||
cfgArgs.ApiToken = cliArgs.ApiToken
|
|
||||||
}
|
|
||||||
if cfgArgs.StorageDir == nil {
|
|
||||||
cfgArgs.StorageDir = cliArgs.StorageDir
|
|
||||||
}
|
|
||||||
return cfgArgs
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig(path string) *args {
|
// overrideWithCliArgs overrides config with non-empty command line arguments
|
||||||
var args args
|
func overrideWithCliArgs(cfg *args, cliConfig *args) {
|
||||||
|
// Only override if the cli value is not empty/zero
|
||||||
|
if cliConfig.Address != nil && *cliConfig.Address != "" {
|
||||||
|
cfg.Address = cliConfig.Address
|
||||||
|
}
|
||||||
|
|
||||||
if !filepath.IsAbs(path) {
|
if cliConfig.Port != nil && *cliConfig.Port != 0 {
|
||||||
|
cfg.Port = cliConfig.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.Username != nil && *cliConfig.Username != "" {
|
||||||
|
cfg.Username = cliConfig.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.Password != nil && *cliConfig.Password != "" {
|
||||||
|
cfg.Password = cliConfig.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.ApiToken != nil && *cliConfig.ApiToken != "" {
|
||||||
|
cfg.ApiToken = cliConfig.ApiToken
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.StorageDir != nil && *cliConfig.StorageDir != "" {
|
||||||
|
cfg.StorageDir = cliConfig.StorageDir
|
||||||
|
}
|
||||||
|
|
||||||
|
if cliConfig.WhiteDownloadDirs != nil {
|
||||||
|
cfg.WhiteDownloadDirs = cliConfig.WhiteDownloadDirs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setDefaults sets default values for any unset configuration fields
|
||||||
|
func setDefaults(cfg *args) {
|
||||||
|
if cfg.Address == nil {
|
||||||
|
address := "0.0.0.0"
|
||||||
|
cfg.Address = &address
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Port == nil {
|
||||||
|
port := 9999
|
||||||
|
cfg.Port = &port
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Username == nil {
|
||||||
|
username := "gopeed"
|
||||||
|
cfg.Username = &username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfigFile loads configuration from file
|
||||||
|
func loadConfigFile(cfg *args, configPath string) {
|
||||||
|
if !filepath.IsAbs(configPath) {
|
||||||
dir, err := os.Getwd()
|
dir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("config dir get failed, reason:" + err.Error())
|
return
|
||||||
return &args
|
|
||||||
}
|
}
|
||||||
path = filepath.Join(dir, path)
|
configPath = filepath.Join(dir, configPath)
|
||||||
}
|
}
|
||||||
file, err := os.ReadFile(path)
|
|
||||||
|
file, err := os.ReadFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return &args
|
return
|
||||||
}
|
}
|
||||||
fmt.Println("config file read failed, reason:" + err.Error())
|
return
|
||||||
return &args
|
|
||||||
}
|
}
|
||||||
if err = json.Unmarshal(file, &args); err != nil {
|
|
||||||
fmt.Println("config file parse failed, reason:" + err.Error())
|
if err = json.Unmarshal(file, cfg); err != nil {
|
||||||
return &args
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadEnvVars loads configuration from environment variables with prefix GOPEED_
|
||||||
|
func loadEnvVars(cfg *args) {
|
||||||
|
v := reflect.ValueOf(cfg).Elem()
|
||||||
|
t := reflect.TypeOf(cfg).Elem()
|
||||||
|
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
field := v.Field(i)
|
||||||
|
fieldType := t.Field(i)
|
||||||
|
|
||||||
|
// Get json tag as environment variable suffix
|
||||||
|
jsonTag := fieldType.Tag.Get("json")
|
||||||
|
if jsonTag == "" || jsonTag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove options like omitempty
|
||||||
|
if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
|
||||||
|
jsonTag = jsonTag[:commaIdx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to uppercase and add GOPEED_ prefix
|
||||||
|
envKey := "GOPEED_" + strings.ToUpper(jsonTag)
|
||||||
|
envValue := os.Getenv(envKey)
|
||||||
|
|
||||||
|
if envValue == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set value based on field type
|
||||||
|
if field.Kind() == reflect.Ptr {
|
||||||
|
if field.IsNil() {
|
||||||
|
// Create new pointer instance
|
||||||
|
newVal := reflect.New(field.Type().Elem())
|
||||||
|
field.Set(newVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch field.Type().Elem().Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
field.Elem().SetString(envValue)
|
||||||
|
case reflect.Int:
|
||||||
|
if intVal, err := strconv.Atoi(envValue); err == nil {
|
||||||
|
field.Elem().SetInt(int64(intVal))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// For complex types like DownloadConfig, try JSON unmarshaling
|
||||||
|
if field.Type().Elem() == reflect.TypeOf(base.DownloaderStoreConfig{}) {
|
||||||
|
var config base.DownloaderStoreConfig
|
||||||
|
if err := json.Unmarshal([]byte(envValue), &config); err == nil {
|
||||||
|
field.Set(reflect.ValueOf(&config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if field.Kind() == reflect.Slice {
|
||||||
|
// Handle non-pointer slice types (like []string for WhiteDownloadDirs)
|
||||||
|
if field.Type().Elem().Kind() == reflect.String {
|
||||||
|
dirs := strings.Split(envValue, ",")
|
||||||
|
for i := range dirs {
|
||||||
|
dirs[i] = strings.TrimSpace(dirs[i])
|
||||||
|
}
|
||||||
|
field.Set(reflect.ValueOf(dirs))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("config file loaded: %s\n", path)
|
|
||||||
return &args
|
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+19
-17
@@ -6,11 +6,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/cmd"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/cmd"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/rest/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist/*
|
//go:embed dist/*
|
||||||
@@ -23,36 +24,37 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
args := parse()
|
args := parse()
|
||||||
var webBasicAuth *model.WebBasicAuth
|
var webAuth *model.WebAuth
|
||||||
if isNotBlank(args.Username) && isNotBlank(args.Password) {
|
if isNotBlank(args.Username) && isNotBlank(args.Password) {
|
||||||
webBasicAuth = &model.WebBasicAuth{
|
webAuth = &model.WebAuth{
|
||||||
Username: *args.Username,
|
Username: *args.Username,
|
||||||
Password: *args.Password,
|
Password: *args.Password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dir string
|
var storageDir string
|
||||||
if args.StorageDir != nil && *args.StorageDir != "" {
|
if args.StorageDir != nil && *args.StorageDir != "" {
|
||||||
dir = *args.StorageDir
|
storageDir = *args.StorageDir
|
||||||
} else {
|
} else {
|
||||||
exe, err := os.Executable()
|
exe, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
dir = filepath.Dir(exe)
|
storageDir = filepath.Join(filepath.Dir(exe), "storage")
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := &model.StartConfig{
|
cfg := &model.StartConfig{
|
||||||
Network: "tcp",
|
Network: "tcp",
|
||||||
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
|
||||||
Storage: model.StorageBolt,
|
Storage: model.StorageBolt,
|
||||||
StorageDir: filepath.Join(dir, "storage"),
|
StorageDir: storageDir,
|
||||||
ApiToken: *args.ApiToken,
|
WhiteDownloadDirs: args.WhiteDownloadDirs,
|
||||||
DownloadConfig: args.DownloadConfig,
|
ApiToken: *args.ApiToken,
|
||||||
ProductionMode: true,
|
DownloadConfig: args.DownloadConfig,
|
||||||
WebEnable: true,
|
ProductionMode: true,
|
||||||
WebFS: sub,
|
WebEnable: true,
|
||||||
WebBasicAuth: webBasicAuth,
|
WebFS: sub,
|
||||||
|
WebAuth: webAuth,
|
||||||
}
|
}
|
||||||
cmd.Start(cfg)
|
cmd.Start(cfg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ require (
|
|||||||
github.com/go-git/go-git/v5 v5.8.1
|
github.com/go-git/go-git/v5 v5.8.1
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
|
github.com/imroc/req/v3 v3.52.2
|
||||||
github.com/matoous/go-nanoid/v2 v2.0.0
|
github.com/matoous/go-nanoid/v2 v2.0.0
|
||||||
github.com/mattn/go-ieproxy v0.0.12
|
github.com/mattn/go-ieproxy v0.0.12
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
@@ -21,17 +22,29 @@ require (
|
|||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||||
go.etcd.io/bbolt v1.3.11
|
go.etcd.io/bbolt v1.3.11
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
||||||
golang.org/x/sync v0.9.0
|
golang.org/x/sync v0.13.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/ebitengine/purego v0.8.1 // indirect
|
github.com/ebitengine/purego v0.8.1 // indirect
|
||||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
|
github.com/icholy/digest v1.1.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||||
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.51.0 // indirect
|
||||||
|
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -61,7 +74,7 @@ require (
|
|||||||
github.com/bits-and-blooms/bitset v1.15.0 // indirect
|
github.com/bits-and-blooms/bitset v1.15.0 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
@@ -77,7 +90,7 @@ require (
|
|||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
@@ -122,13 +135,13 @@ require (
|
|||||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||||
golang.org/x/crypto v0.29.0 // indirect
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
golang.org/x/mod v0.22.0 // indirect
|
golang.org/x/mod v0.24.0 // indirect
|
||||||
golang.org/x/net v0.31.0 // indirect
|
golang.org/x/net v0.39.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
golang.org/x/text v0.20.0 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
golang.org/x/time v0.8.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.27.0 // indirect
|
golang.org/x/tools v0.32.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
modernc.org/libc v1.61.0 // indirect
|
modernc.org/libc v1.61.0 // indirect
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ github.com/anacrolix/upnp v0.1.4 h1:+2t2KA6QOhm/49zeNyeVwDu1ZYS9dB9wfxyVvh/wk7U=
|
|||||||
github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
|
github.com/anacrolix/upnp v0.1.4/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984+9fIic=
|
||||||
github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
|
github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
|
||||||
github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8=
|
github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8=
|
||||||
|
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||||
|
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
@@ -122,8 +124,9 @@ github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4M
|
|||||||
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
|
||||||
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
|
||||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
|
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -196,6 +199,8 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs
|
|||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||||
@@ -229,12 +234,12 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ=
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 h1:gD0vax+4I+mAj+jEChEf25Ia07Jq7kYOFO5PPhAxFl4=
|
||||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
@@ -250,6 +255,11 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
|||||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
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/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
@@ -262,6 +272,10 @@ github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
|
|||||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
|
||||||
|
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
||||||
|
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||||
|
github.com/imroc/req/v3 v3.52.2 h1:xJocr1aIv0a2K9knfBQ4JnZHk+kWTITdjf0mgDg229I=
|
||||||
|
github.com/imroc/req/v3 v3.52.2/go.mod h1:dBGsDloOSZJcFs6PnTjZXYBJK70OXbZpizHBLNqcH2k=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||||
@@ -277,6 +291,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
|
|||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
@@ -331,7 +347,11 @@ github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJm
|
|||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||||
|
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
|
||||||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
@@ -380,6 +400,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
|
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
|
||||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||||
|
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
@@ -404,7 +426,13 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
|||||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||||
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
|
github.com/protolambda/ctxlock v0.1.0 h1:rCUY3+vRdcdZXqT07iXgyr744J2DU2LCBIXowYAjBCE=
|
||||||
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
|
github.com/protolambda/ctxlock v0.1.0/go.mod h1:vefhX6rIZH8rsg5ZpOJfEDYQOppZi19SfPiGOFrNnwM=
|
||||||
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.51.0 h1:K8exxe9zXxeRKxaXxi/GpUqYiTrtdiWP8bo1KFya6Wc=
|
||||||
|
github.com/quic-go/quic-go v0.51.0/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ=
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||||
|
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
||||||
|
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
@@ -452,8 +480,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
|||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
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/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
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/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
|
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
|
||||||
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
||||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||||
@@ -471,6 +500,8 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
|
|||||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad h1:QtuWk6dsrNXc/ugwCBEN0jG52q/4tXRzMvZm4fH6T9g=
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad h1:QtuWk6dsrNXc/ugwCBEN0jG52q/4tXRzMvZm4fH6T9g=
|
||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI=
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgKGTPQ+M8CdUMS9p7Mmdoxa3OAcAjJQBu0CcI=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
@@ -487,8 +518,12 @@ go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZk
|
|||||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||||
|
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||||
|
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
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/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
|
||||||
|
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
@@ -497,8 +532,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
|
|||||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||||
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
|
||||||
@@ -512,8 +547,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
|||||||
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
@@ -535,8 +570,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
|||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
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-20190226205417-e64efc72b421/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-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -550,8 +585,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
@@ -590,17 +625,15 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
|
||||||
golang.org/x/sys v0.28.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.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||||
golang.org/x/text v0.3.0/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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
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.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -609,8 +642,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -624,8 +657,8 @@ golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4f
|
|||||||
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
|
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||||
golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
|
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -646,6 +679,8 @@ google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miE
|
|||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -667,6 +702,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
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-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||||
|
|||||||
@@ -24,8 +24,10 @@ type DefaultFileController struct {
|
|||||||
|
|
||||||
func NewController() *Controller {
|
func NewController() *Controller {
|
||||||
return &Controller{
|
return &Controller{
|
||||||
GetConfig: func(v any) {},
|
GetConfig: func(v any) {},
|
||||||
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil },
|
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) {
|
||||||
|
return requestProxy.ToHandler()
|
||||||
|
},
|
||||||
FileController: &DefaultFileController{},
|
FileController: &DefaultFileController{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ type config struct {
|
|||||||
ListenPort int `json:"listenPort"`
|
ListenPort int `json:"listenPort"`
|
||||||
Trackers []string `json:"trackers"`
|
Trackers []string `json:"trackers"`
|
||||||
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
|
// SeedKeep is always keep seeding after downloading is complete, unless manually stopped.
|
||||||
SeedKeep bool `json:"seedKeep`
|
SeedKeep bool `json:"seedKeep"`
|
||||||
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
// SeedRatio is the ratio of uploaded data to downloaded data to seed.
|
||||||
SeedRatio float64 `json:"seedRatio"`
|
SeedRatio float64 `json:"seedRatio"`
|
||||||
// SeedTime is the time in seconds to seed after downloading is complete.
|
// SeedTime is the time in seconds to seed after downloading is complete.
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
|
||||||
"github.com/anacrolix/torrent"
|
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -19,6 +12,14 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"github.com/anacrolix/torrent"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -100,9 +101,6 @@ func (f *Fetcher) initClient() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Resolve(req *base.Request) error {
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||||
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.meta.Req = req
|
f.meta.Req = req
|
||||||
if err := f.addTorrent(req, false); err != nil {
|
if err := f.addTorrent(req, false); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -355,10 +353,14 @@ func (f *Fetcher) WaitUpload() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
||||||
|
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err = f.initClient(); err != nil {
|
if err = f.initClient(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
schema := util.ParseSchema(req.URL)
|
schema := util.ParseSchema(req.URL)
|
||||||
|
privateTorrent := false
|
||||||
if schema == "MAGNET" {
|
if schema == "MAGNET" {
|
||||||
f.torrent, err = client.AddMagnet(req.URL)
|
f.torrent, err = client.AddMagnet(req.URL)
|
||||||
} else {
|
} else {
|
||||||
@@ -388,34 +390,46 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
|||||||
if err != nil && !strings.Contains(err.Error(), "expected EOF") {
|
if err != nil && !strings.Contains(err.Error(), "expected EOF") {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info, er := metaInfo.UnmarshalInfo()
|
||||||
|
if er != nil {
|
||||||
|
return er
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Private != nil && *info.Private {
|
||||||
|
privateTorrent = true
|
||||||
|
}
|
||||||
f.torrent, err = client.AddTorrent(metaInfo)
|
f.torrent, err = client.AddTorrent(metaInfo)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// use map to deduplicate
|
// Do not add external tracker to a private torrent.
|
||||||
trackers := make(map[string]bool)
|
if !privateTorrent {
|
||||||
if req.Extra != nil {
|
// use map to deduplicate
|
||||||
extra := req.Extra.(*bt.ReqExtra)
|
trackers := make(map[string]bool)
|
||||||
if len(extra.Trackers) > 0 {
|
if req.Extra != nil {
|
||||||
for _, tracker := range extra.Trackers {
|
extra := req.Extra.(*bt.ReqExtra)
|
||||||
|
if len(extra.Trackers) > 0 {
|
||||||
|
for _, tracker := range extra.Trackers {
|
||||||
|
trackers[tracker] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(f.config.Trackers) > 0 {
|
||||||
|
for _, tracker := range f.config.Trackers {
|
||||||
trackers[tracker] = true
|
trackers[tracker] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if len(trackers) > 0 {
|
||||||
if len(f.config.Trackers) > 0 {
|
announceList := make([][]string, 0)
|
||||||
for _, tracker := range f.config.Trackers {
|
for tracker := range trackers {
|
||||||
trackers[tracker] = true
|
announceList = append(announceList, []string{tracker})
|
||||||
|
}
|
||||||
|
f.torrent.AddTrackers(announceList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(trackers) > 0 {
|
|
||||||
announceList := make([][]string, 0)
|
|
||||||
for tracker := range trackers {
|
|
||||||
announceList = append(announceList, []string{tracker})
|
|
||||||
}
|
|
||||||
f.torrent.AddTrackers(announceList)
|
|
||||||
}
|
|
||||||
<-f.torrent.GotInfo()
|
<-f.torrent.GotInfo()
|
||||||
f.torrentReady.Store(true)
|
f.torrentReady.Store(true)
|
||||||
|
|
||||||
|
|||||||
+248
-111
File diff suppressed because it is too large.
Load diff
@@ -47,6 +47,26 @@ func TestFetcher_Resolve(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, t)
|
}, t)
|
||||||
|
testResolve(test.StartTestCustomServer, "no-encode", &base.Resource{
|
||||||
|
Size: test.BuildSize,
|
||||||
|
Range: false,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "测试.zip",
|
||||||
|
Size: test.BuildSize,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, t)
|
||||||
|
testResolve(test.StartTestCustomServer, "%E6%B5%8B%E8%AF%95.zip", &base.Resource{
|
||||||
|
Size: 0,
|
||||||
|
Range: false,
|
||||||
|
Files: []*base.FileInfo{
|
||||||
|
{
|
||||||
|
Name: "测试.zip",
|
||||||
|
Size: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, t)
|
||||||
testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{
|
testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{
|
||||||
Size: 0,
|
Size: 0,
|
||||||
Range: false,
|
Range: false,
|
||||||
@@ -85,6 +105,21 @@ func TestFetcher_ResolveWithHostHeader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_ResolveWithInvalidHeader(t *testing.T) {
|
||||||
|
fetcher := buildFetcher()
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "https://bing.com",
|
||||||
|
Extra: &http.ReqExtra{
|
||||||
|
Header: map[string]string{
|
||||||
|
"Referer": "\rtest",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Resolve() got = %v, want nil", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
|
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
|
||||||
listener := startTestServer()
|
listener := startTestServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -166,6 +201,14 @@ func TestFetcher_DownloadResponseBodyReadTimeout(t *testing.T) {
|
|||||||
downloadError(listener, 4, t)
|
downloadError(listener, 4, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_DownloadOnBugFileServer(t *testing.T) {
|
||||||
|
listener := test.StartTestRangeBugServer()
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloadNormal(listener, 1, t)
|
||||||
|
downloadNormal(listener, 4, t)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFetcher_DownloadResume(t *testing.T) {
|
func TestFetcher_DownloadResume(t *testing.T) {
|
||||||
listener := test.StartTestFileServer()
|
listener := test.StartTestFileServer()
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
@@ -228,6 +271,33 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_Stats(t *testing.T) {
|
||||||
|
listener := test.StartTestFileServer()
|
||||||
|
defer listener.Close()
|
||||||
|
fetcher := doDownloadReady(buildConfigFetcher(config{
|
||||||
|
Connections: 16,
|
||||||
|
}), listener, 0, t)
|
||||||
|
err := fetcher.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
err = fetcher.Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
stats := fetcher.Stats().(*http.Stats)
|
||||||
|
if len(stats.Connections) != 16 {
|
||||||
|
t.Errorf("Stats() got = %v, want %v", len(stats.Connections), 16)
|
||||||
|
}
|
||||||
|
totalDownloaded := int64(0)
|
||||||
|
for _, conn := range stats.Connections {
|
||||||
|
totalDownloaded += conn.Downloaded
|
||||||
|
}
|
||||||
|
if totalDownloaded != test.BuildSize {
|
||||||
|
t.Errorf("Stats() got = %v, want %v", totalDownloaded, test.BuildSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFetcherManager_ParseName(t *testing.T) {
|
func TestFetcherManager_ParseName(t *testing.T) {
|
||||||
type args struct {
|
type args struct {
|
||||||
u string
|
u string
|
||||||
|
|||||||
+106
-57
@@ -94,6 +94,25 @@ func StartTestCustomServer() net.Listener {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
io.Copy(writer, file)
|
io.Copy(writer, file)
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/%E6%B5%8B%E8%AF%95.zip", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/no-encode", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
writer.Header().Set("Content-Disposition", "attachment; filename=测试.zip")
|
||||||
|
writer.Header().Set("Content-Type", "application/octet-stream")
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
io.Copy(writer, file)
|
||||||
|
})
|
||||||
return mux
|
return mux
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -172,68 +191,98 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
|||||||
writer.WriteHeader(403)
|
writer.WriteHeader(403)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
rangeFileHandle(
|
||||||
r := request.Header.Get("Range")
|
writer,
|
||||||
if r == "" {
|
request,
|
||||||
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
nil,
|
||||||
writer.WriteHeader(200)
|
func(file *os.File, n int64) {
|
||||||
(writer.(http.Flusher)).Flush()
|
slowCopyN(sl, writer, file, n, delay)
|
||||||
|
},
|
||||||
file, err := os.Open(BuildFile)
|
)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
slowCopy(sl, writer, file, delay)
|
|
||||||
} else {
|
|
||||||
// split range
|
|
||||||
s := strings.Split(r, "=")
|
|
||||||
if len(s) != 2 {
|
|
||||||
writer.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s = strings.Split(s[1], "-")
|
|
||||||
if len(s) != 2 {
|
|
||||||
writer.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
start, err := strconv.ParseInt(s[0], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
writer.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
end, err := strconv.ParseInt(s[1], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
writer.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if start < 0 || end < 0 || start > end {
|
|
||||||
writer.WriteHeader(400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if end >= BuildSize {
|
|
||||||
end = BuildSize - 1
|
|
||||||
}
|
|
||||||
writer.Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
|
|
||||||
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
|
||||||
writer.Header().Set("Accept-Ranges", "bytes")
|
|
||||||
writer.WriteHeader(206)
|
|
||||||
(writer.(http.Flusher)).Flush()
|
|
||||||
|
|
||||||
file, err := os.Open(BuildFile)
|
|
||||||
if err != nil {
|
|
||||||
writer.WriteHeader(500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
file.Seek(start, 0)
|
|
||||||
slowCopyN(sl, writer, file, end-start+1, delay)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return mux
|
return mux
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartTestRangeBugServer simulate bug server:
|
||||||
|
// Don't follow Range request rules, always return more data than range, e.g. Range: bytes=0-100, return 150 bytes
|
||||||
|
func StartTestRangeBugServer() net.Listener {
|
||||||
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
rangeFileHandle(
|
||||||
|
writer,
|
||||||
|
request,
|
||||||
|
func(end int64) int64 {
|
||||||
|
var bugEnd = end
|
||||||
|
if end != 0 {
|
||||||
|
bugEnd = end + 50
|
||||||
|
if bugEnd >= BuildSize {
|
||||||
|
bugEnd = end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bugEnd
|
||||||
|
},
|
||||||
|
func(file *os.File, n int64) {
|
||||||
|
io.CopyN(writer, file, n)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return mux
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func rangeFileHandle(writer http.ResponseWriter, request *http.Request, modifyEnd func(end int64) int64, iocpN func(file *os.File, n int64)) {
|
||||||
|
r := request.Header.Get("Range")
|
||||||
|
// split range
|
||||||
|
s := strings.Split(r, "=")
|
||||||
|
if len(s) != 2 {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = strings.Split(s[1], "-")
|
||||||
|
if len(s) != 2 {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
start, err := strconv.ParseInt(s[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
end, err := strconv.ParseInt(s[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if start < 0 || end < 0 || start > end {
|
||||||
|
writer.WriteHeader(400)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if end >= BuildSize {
|
||||||
|
end = BuildSize - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if modifyEnd != nil {
|
||||||
|
end = modifyEnd(end)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
|
||||||
|
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
||||||
|
writer.Header().Set("Accept-Ranges", "bytes")
|
||||||
|
writer.WriteHeader(206)
|
||||||
|
(writer.(http.Flusher)).Flush()
|
||||||
|
|
||||||
|
file, err := os.Open(BuildFile)
|
||||||
|
if err != nil {
|
||||||
|
writer.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
file.Seek(start, 0)
|
||||||
|
iocpN(file, end-start+1)
|
||||||
|
}
|
||||||
|
|
||||||
// slowCopyN copies n bytes from src to dst, speed limit is bytes per second
|
// slowCopyN copies n bytes from src to dst, speed limit is bytes per second
|
||||||
func slowCopy(sl *shutdownListener, dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
|
func slowCopy(sl *shutdownListener, dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
|
||||||
buf := make([]byte, 32*1024)
|
buf := make([]byte, 32*1024)
|
||||||
|
|||||||
@@ -163,6 +163,16 @@ func ParseOptsExtra[E any](opts *Options) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateTaskBatch struct {
|
||||||
|
Reqs []*CreateTaskBatchItem `json:"reqs"`
|
||||||
|
Opts *Options `json:"opts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateTaskBatchItem struct {
|
||||||
|
Req *Request `json:"req"`
|
||||||
|
Opts *Options `json:"opts"`
|
||||||
|
}
|
||||||
|
|
||||||
// DownloaderStoreConfig is the config that can restore the downloader.
|
// DownloaderStoreConfig is the config that can restore the downloader.
|
||||||
type DownloaderStoreConfig struct {
|
type DownloaderStoreConfig struct {
|
||||||
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
|
||||||
|
|||||||
+27
-20
@@ -148,6 +148,7 @@ func (d *Downloader) Setup() error {
|
|||||||
tasks = append(tasks[:i], tasks[i+1:]...)
|
tasks = append(tasks[:i], tasks[i+1:]...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
d.assignFetcherManager(task)
|
||||||
initTask(task)
|
initTask(task)
|
||||||
if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError {
|
if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError {
|
||||||
task.Status = base.DownloadStatusPause
|
task.Status = base.DownloadStatusPause
|
||||||
@@ -208,7 +209,7 @@ func (d *Downloader) Setup() error {
|
|||||||
if task.Status == base.DownloadStatusRunning {
|
if task.Status == base.DownloadStatusRunning {
|
||||||
downloadDataChanged = current != task.Progress.Downloaded
|
downloadDataChanged = current != task.Progress.Downloaded
|
||||||
task.Progress.Used = task.timer.Used()
|
task.Progress.Used = task.timer.Used()
|
||||||
task.Progress.Speed = task.calcSpeed(task.speedArr, current-task.Progress.Downloaded, tick)
|
task.Progress.Speed = task.updateSpeed(current-task.Progress.Downloaded, tick)
|
||||||
task.Progress.Downloaded = current
|
task.Progress.Downloaded = current
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +218,7 @@ func (d *Downloader) Setup() error {
|
|||||||
uploader := task.fetcher.(fetcher.Uploader)
|
uploader := task.fetcher.(fetcher.Uploader)
|
||||||
currentUploaded := uploader.UploadedBytes()
|
currentUploaded := uploader.UploadedBytes()
|
||||||
uploadDataChanged = currentUploaded != task.Progress.Uploaded
|
uploadDataChanged = currentUploaded != task.Progress.Uploaded
|
||||||
task.Progress.UploadSpeed = task.calcSpeed(task.uploadSpeedArr, currentUploaded-task.Progress.Uploaded, tick)
|
task.Progress.UploadSpeed = task.updateUploadSpeed(currentUploaded-task.Progress.Uploaded, tick)
|
||||||
task.Progress.Uploaded = currentUploaded
|
task.Progress.Uploaded = currentUploaded
|
||||||
}
|
}
|
||||||
d.emit(EventKeyProgress, task)
|
d.emit(EventKeyProgress, task)
|
||||||
@@ -358,10 +359,14 @@ func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId
|
|||||||
return d.doCreate(fetcher, opts)
|
return d.doCreate(fetcher, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) {
|
func (d *Downloader) CreateDirectBatch(req *base.CreateTaskBatch) (taskId []string, err error) {
|
||||||
taskIds := make([]string, 0)
|
taskIds := make([]string, 0)
|
||||||
for _, req := range reqs {
|
for _, ir := range req.Reqs {
|
||||||
taskId, err := d.CreateDirect(req, opts.Clone())
|
opts := ir.Opts
|
||||||
|
if opts == nil {
|
||||||
|
opts = req.Opts
|
||||||
|
}
|
||||||
|
taskId, err := d.CreateDirect(ir.Req, opts.Clone())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -573,15 +578,19 @@ func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) deleteAll() (err error) {
|
func (d *Downloader) deleteAll() (err error) {
|
||||||
|
var deleteTasksTemp []*Task
|
||||||
func() {
|
func() {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
defer d.lock.Unlock()
|
defer d.lock.Unlock()
|
||||||
|
|
||||||
|
for _, task := range d.tasks {
|
||||||
|
deleteTasksTemp = append(deleteTasksTemp, task)
|
||||||
|
}
|
||||||
d.tasks = make([]*Task, 0)
|
d.tasks = make([]*Task, 0)
|
||||||
d.waitTasks = make([]*Task, 0)
|
d.waitTasks = make([]*Task, 0)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, task := range d.tasks {
|
for _, task := range deleteTasksTemp {
|
||||||
if err = d.doDelete(task, true); err != nil {
|
if err = d.doDelete(task, true); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -892,18 +901,7 @@ func (d *Downloader) restoreTask(task *Task) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) restoreFetcher(task *Task) error {
|
func (d *Downloader) restoreFetcher(task *Task) error {
|
||||||
var fm fetcher.FetcherManager
|
v, f := task.fetcherManager.Restore()
|
||||||
for _, f := range d.cfg.FetchManagers {
|
|
||||||
if f.Name() == task.Protocol {
|
|
||||||
fm = f
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fm == nil {
|
|
||||||
return ErrUnSupportedProtocol
|
|
||||||
}
|
|
||||||
task.fetcherManager = fm
|
|
||||||
v, f := fm.Restore()
|
|
||||||
if v != nil {
|
if v != nil {
|
||||||
err := d.storage.Pop(bucketSave, task.ID, v)
|
err := d.storage.Pop(bucketSave, task.ID, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -943,9 +941,9 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if enable white download directory, check if the download directory is in the white list
|
// if enable white download directory, check if the download directory is in the white list
|
||||||
if len(d.cfg.DownloadDirWhiteList) > 0 {
|
if len(d.cfg.WhiteDownloadDirs) > 0 {
|
||||||
inWhiteList := false
|
inWhiteList := false
|
||||||
for _, dir := range d.cfg.DownloadDirWhiteList {
|
for _, dir := range d.cfg.WhiteDownloadDirs {
|
||||||
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
|
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
|
||||||
inWhiteList = true
|
inWhiteList = true
|
||||||
break
|
break
|
||||||
@@ -1145,6 +1143,15 @@ func logPanic(logDir string) {
|
|||||||
debug.SetCrashOutput(f, debug.CrashOptions{})
|
debug.SetCrashOutput(f, debug.CrashOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) assignFetcherManager(task *Task) error {
|
||||||
|
fm, err := d.parseFm(task.Meta.Req.URL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
task.fetcherManager = fm
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
||||||
fm, err := d.parseFm(url)
|
fm, err := d.parseFm(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func TestDownloader_CreateNotInWhite(t *testing.T) {
|
|||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
|
||||||
downloader := NewDownloader(&DownloaderConfig{
|
downloader := NewDownloader(&DownloaderConfig{
|
||||||
DownloadDirWhiteList: []string{"./downloads"},
|
WhiteDownloadDirs: []string{"./downloads"},
|
||||||
})
|
})
|
||||||
if err := downloader.Setup(); err != nil {
|
if err := downloader.Setup(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -137,13 +137,15 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
downloader.Clear()
|
downloader.Clear()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
reqs := make([]*base.Request, 0)
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
fileNames := make([]string, 0)
|
fileNames := make([]string, 0)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
req := &base.Request{
|
req := &base.Request{
|
||||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
}
|
}
|
||||||
reqs = append(reqs, req)
|
reqs = append(reqs, &base.CreateTaskBatchItem{
|
||||||
|
Req: req,
|
||||||
|
})
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
fileNames = append(fileNames, test.DownloadName)
|
fileNames = append(fileNames, test.DownloadName)
|
||||||
} else {
|
} else {
|
||||||
@@ -160,11 +162,14 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
_, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
_, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
|
||||||
Path: test.Dir,
|
Reqs: reqs,
|
||||||
Name: test.DownloadName,
|
Opts: &base.Options{
|
||||||
Extra: http.OptsExtra{
|
Path: test.Dir,
|
||||||
Connections: 4,
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -499,13 +504,15 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
|||||||
downloader.Clear()
|
downloader.Clear()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
reqs := make([]*base.Request, 0)
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
fileNames := make([]string, 0)
|
fileNames := make([]string, 0)
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
req := &base.Request{
|
req := &base.Request{
|
||||||
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
|
||||||
}
|
}
|
||||||
reqs = append(reqs, req)
|
reqs = append(reqs, &base.CreateTaskBatchItem{
|
||||||
|
Req: req,
|
||||||
|
})
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
fileNames = append(fileNames, test.DownloadName)
|
fileNames = append(fileNames, test.DownloadName)
|
||||||
} else {
|
} else {
|
||||||
@@ -522,11 +529,14 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
taskIds, err := downloader.CreateDirectBatch(reqs, &base.Options{
|
taskIds, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
|
||||||
Path: test.Dir,
|
Reqs: reqs,
|
||||||
Name: test.DownloadName,
|
Opts: &base.Options{
|
||||||
Extra: http.OptsExtra{
|
Path: test.Dir,
|
||||||
Connections: 4,
|
Name: test.DownloadName,
|
||||||
|
Extra: http.OptsExtra{
|
||||||
|
Connections: 4,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -205,6 +205,31 @@ function testTimeout(){
|
|||||||
xhr.send();
|
xhr.send();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function testFingerprint(fingerprint,ua){
|
||||||
|
__gopeed_setFingerprint(fingerprint);
|
||||||
|
const resp = await fetch(host+'/ua');
|
||||||
|
const data = await resp.json();
|
||||||
|
if(!data.user_agent.includes(ua)){
|
||||||
|
throw new Error('fingerprint test failed, user agent: ' + data.user_agent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintDefault(){
|
||||||
|
await testFingerprint('none', 'Go')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintChrome(){
|
||||||
|
await testFingerprint('chrome', 'Chrome')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintFirefox(){
|
||||||
|
await testFingerprint('firefox', 'Firefox')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testFingerprintSafari(){
|
||||||
|
await testFingerprint('safari', 'Safari')
|
||||||
|
}
|
||||||
`)
|
`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -288,6 +313,19 @@ function testTimeout(){
|
|||||||
if err == nil || err.Error() != "timeout" {
|
if err == nil || err.Error() != "timeout" {
|
||||||
t.Fatalf("timeout test failed, want %s, got %s", "timeout", err)
|
t.Fatalf("timeout test failed, want %s, got %s", "timeout", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = callTestFun(engine, "testFingerprintChrome")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("testFingerprintChrome test failed", err)
|
||||||
|
}
|
||||||
|
_, err = callTestFun(engine, "testFingerprintFirefox")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("testFingerprintFirefox test failed", err)
|
||||||
|
}
|
||||||
|
_, err = callTestFun(engine, "testFingerprintSafari")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("testFingerprintSafari test failed", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFetchWithProxy(t *testing.T) {
|
func TestFetchWithProxy(t *testing.T) {
|
||||||
@@ -470,6 +508,15 @@ func startServer() net.Listener {
|
|||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/ua", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
data := map[string]any{
|
||||||
|
"user_agent": r.UserAgent(),
|
||||||
|
}
|
||||||
|
buf, _ := json.Marshal(data)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Write(buf)
|
||||||
|
})
|
||||||
server.Handler = mux
|
server.Handler = mux
|
||||||
go server.Serve(listener)
|
go server.Serve(listener)
|
||||||
return listener
|
return listener
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
|
||||||
"github.com/dop251/goja"
|
"github.com/dop251/goja"
|
||||||
|
"github.com/imroc/req/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -129,7 +130,7 @@ type XMLHttpRequest struct {
|
|||||||
requestHeaders http.Header
|
requestHeaders http.Header
|
||||||
responseHeaders http.Header
|
responseHeaders http.Header
|
||||||
aborted bool
|
aborted bool
|
||||||
proxyHandler func(r *http.Request) (*url.URL, error)
|
client *req.Client
|
||||||
|
|
||||||
WithCredentials bool `json:"withCredentials"`
|
WithCredentials bool `json:"withCredentials"`
|
||||||
Upload *XMLHttpRequestUpload `json:"upload"`
|
Upload *XMLHttpRequestUpload `json:"upload"`
|
||||||
@@ -163,25 +164,39 @@ func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
||||||
var req *http.Request
|
setFingerprint(xhr.client)
|
||||||
var err error
|
|
||||||
d := xhr.parseData(data)
|
d := xhr.parseData(data)
|
||||||
var (
|
var (
|
||||||
contentType string
|
contentType string
|
||||||
contentLength int64
|
contentLength int64
|
||||||
isStringBody bool
|
isStringBody bool
|
||||||
)
|
)
|
||||||
if d == nil || xhr.method == "GET" || xhr.method == "HEAD" {
|
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, nil)
|
// Create request using req library
|
||||||
} else {
|
reqBuilder := xhr.client.R()
|
||||||
|
|
||||||
|
// Set headers first
|
||||||
|
if xhr.requestHeaders != nil {
|
||||||
|
for key, values := range xhr.requestHeaders {
|
||||||
|
if len(values) > 0 {
|
||||||
|
// Merge multiple values with comma separator (HTTP standard)
|
||||||
|
mergedValue := strings.Join(values, ", ")
|
||||||
|
reqBuilder.SetHeader(key, mergedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle request body
|
||||||
|
if d != nil && xhr.method != "GET" && xhr.method != "HEAD" {
|
||||||
switch v := d.(type) {
|
switch v := d.(type) {
|
||||||
case string:
|
case string:
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, bytes.NewBufferString(v))
|
reqBuilder.SetBody(v)
|
||||||
contentType = "text/plain;charset=UTF-8"
|
contentType = "text/plain;charset=UTF-8"
|
||||||
contentLength = int64(len(v))
|
contentLength = int64(len(v))
|
||||||
isStringBody = true
|
isStringBody = true
|
||||||
case *file.File:
|
case *file.File:
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, v.Reader)
|
reqBuilder.SetBody(v.Reader)
|
||||||
contentType = "application/octet-stream"
|
contentType = "application/octet-stream"
|
||||||
contentLength = v.Size
|
contentLength = v.Size
|
||||||
case *formdata.FormData:
|
case *formdata.FormData:
|
||||||
@@ -203,43 +218,38 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
defer mw.Close()
|
defer mw.Close()
|
||||||
mw.Send()
|
mw.Send()
|
||||||
}()
|
}()
|
||||||
req, err = http.NewRequest(xhr.method, xhr.url, pr)
|
reqBuilder.SetBody(pr)
|
||||||
contentType = mw.FormDataContentType()
|
contentType = mw.FormDataContentType()
|
||||||
contentLength = mw.Size()
|
contentLength = mw.Size()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
xhr.callOnerror()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
req.Header = xhr.requestHeaders
|
|
||||||
// Only string body can specify Content-Type header by user
|
// Only string body can specify Content-Type header by user
|
||||||
if contentType != "" && (!isStringBody || req.Header.Get("Content-Type") == "") {
|
if contentType != "" && (!isStringBody || xhr.requestHeaders.Get("Content-Type") == "") {
|
||||||
req.Header.Set("Content-Type", contentType)
|
reqBuilder.SetHeader("Content-Type", contentType)
|
||||||
}
|
}
|
||||||
if contentLength > 0 {
|
|
||||||
req.ContentLength = contentLength
|
// Set timeout
|
||||||
|
if xhr.Timeout > 0 {
|
||||||
|
xhr.client.SetTimeout(time.Duration(xhr.Timeout) * time.Millisecond)
|
||||||
}
|
}
|
||||||
transport := &http.Transport{
|
|
||||||
Proxy: xhr.proxyHandler,
|
// Configure redirect behavior
|
||||||
}
|
xhr.client.SetRedirectPolicy(func(req *http.Request, via []*http.Request) error {
|
||||||
client := &http.Client{
|
if xhr.Redirect == redirectManual {
|
||||||
Transport: transport,
|
return http.ErrUseLastResponse
|
||||||
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
|
}
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
if xhr.Redirect == redirectError {
|
||||||
if xhr.Redirect == redirectManual {
|
return errors.New("redirect failed")
|
||||||
return http.ErrUseLastResponse
|
}
|
||||||
}
|
if len(via) > 20 {
|
||||||
if xhr.Redirect == redirectError {
|
return errors.New("too many redirects")
|
||||||
return errors.New("redirect failed")
|
}
|
||||||
}
|
return nil
|
||||||
if len(via) > 20 {
|
})
|
||||||
return errors.New("too many redirects")
|
|
||||||
}
|
// Execute request
|
||||||
return nil
|
resp, err := reqBuilder.Send(xhr.method, xhr.url)
|
||||||
},
|
|
||||||
}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// handle timeout error
|
// handle timeout error
|
||||||
var ne net.Error
|
var ne net.Error
|
||||||
@@ -254,35 +264,37 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
|
|||||||
xhr.callOnerror()
|
xhr.callOnerror()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
xhr.Upload.callOnprogress(contentLength, contentLength)
|
xhr.Upload.callOnprogress(contentLength, contentLength)
|
||||||
if !xhr.aborted {
|
if !xhr.aborted {
|
||||||
xhr.Upload.callOnload()
|
xhr.Upload.callOnload()
|
||||||
}
|
}
|
||||||
|
|
||||||
responseUrl := resp.Request.URL
|
// Set response URL (final URL after redirects)
|
||||||
responseUrl.Fragment = ""
|
if resp.Response != nil && resp.Response.Request != nil && resp.Response.Request.URL != nil {
|
||||||
xhr.ResponseUrl = responseUrl.String()
|
responseUrl := resp.Response.Request.URL
|
||||||
|
responseUrl.Fragment = ""
|
||||||
|
xhr.ResponseUrl = responseUrl.String()
|
||||||
|
} else {
|
||||||
|
xhr.ResponseUrl = xhr.url
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set response headers
|
||||||
xhr.responseHeaders = resp.Header
|
xhr.responseHeaders = resp.Header
|
||||||
xhr.Status = resp.StatusCode
|
xhr.Status = resp.StatusCode
|
||||||
xhr.StatusText = resp.Status
|
xhr.StatusText = resp.Status
|
||||||
xhr.doReadystatechange(2)
|
xhr.doReadystatechange(2)
|
||||||
buf, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
bodyBytes := resp.Bytes()
|
||||||
xhr.callOnerror()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
xhr.doReadystatechange(3)
|
xhr.doReadystatechange(3)
|
||||||
xhr.Response = string(buf)
|
xhr.Response = string(bodyBytes)
|
||||||
xhr.ResponseText = xhr.Response
|
xhr.ResponseText = xhr.Response
|
||||||
xhr.doReadystatechange(4)
|
xhr.doReadystatechange(4)
|
||||||
respBodyLen := int64(len(buf))
|
respBodyLen := int64(len(bodyBytes))
|
||||||
xhr.callOnprogress(respBodyLen, respBodyLen)
|
xhr.callOnprogress(respBodyLen, respBodyLen)
|
||||||
if !xhr.aborted {
|
if !xhr.aborted {
|
||||||
xhr.callOnload()
|
xhr.callOnload()
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) Abort() {
|
func (xhr *XMLHttpRequest) Abort() {
|
||||||
@@ -293,6 +305,9 @@ func (xhr *XMLHttpRequest) Abort() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
|
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
|
||||||
|
if xhr.responseHeaders == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
return strings.Join(xhr.responseHeaders.Values(key), ", ")
|
return strings.Join(xhr.responseHeaders.Values(key), ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,8 +374,14 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
|
|||||||
return instanceValue
|
return instanceValue
|
||||||
})
|
})
|
||||||
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
|
||||||
|
// Create req client with proxy support
|
||||||
|
client := req.C()
|
||||||
|
if proxyHandler != nil {
|
||||||
|
client.SetProxy(proxyHandler)
|
||||||
|
}
|
||||||
|
|
||||||
instance := &XMLHttpRequest{
|
instance := &XMLHttpRequest{
|
||||||
proxyHandler: proxyHandler,
|
client: client,
|
||||||
Upload: &XMLHttpRequestUpload{
|
Upload: &XMLHttpRequestUpload{
|
||||||
EventProp: &EventProp{
|
EventProp: &EventProp{
|
||||||
eventListeners: make(map[string]func(event *ProgressEvent)),
|
eventListeners: make(map[string]func(event *ProgressEvent)),
|
||||||
@@ -374,6 +395,9 @@ func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL,
|
|||||||
instanceValue.SetPrototype(call.This.Prototype())
|
instanceValue.SetPrototype(call.This.Prototype())
|
||||||
return instanceValue
|
return instanceValue
|
||||||
})
|
})
|
||||||
|
runtime.Set("__gopeed_setFingerprint", func(fingerprint Fingerprint) {
|
||||||
|
currentFingerprint = fingerprint
|
||||||
|
})
|
||||||
if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
|
if err := runtime.Set("ProgressEvent", progressEvent); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package xhr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imroc/req/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Fingerprint string
|
||||||
|
|
||||||
|
const (
|
||||||
|
fingerprintChrome Fingerprint = "chrome"
|
||||||
|
fingerprintFirefox Fingerprint = "firefox"
|
||||||
|
fingerprintSafari Fingerprint = "safari"
|
||||||
|
)
|
||||||
|
|
||||||
|
var currentFingerprint Fingerprint = ""
|
||||||
|
|
||||||
|
func setFingerprint(client *req.Client) {
|
||||||
|
switch currentFingerprint {
|
||||||
|
case fingerprintChrome:
|
||||||
|
client.ImpersonateChrome()
|
||||||
|
case fingerprintFirefox:
|
||||||
|
client.ImpersonateFirefox()
|
||||||
|
case fingerprintSafari:
|
||||||
|
client.ImpersonateSafari()
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-10
@@ -100,16 +100,27 @@ func (t *Task) clone() *Task {
|
|||||||
return util.DeepClone(t)
|
return util.DeepClone(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 {
|
func (t *Task) updateSpeed(downloaded int64, usedTime float64) int64 {
|
||||||
speedArr = append(speedArr, downloaded)
|
return calcSpeed(&t.speedArr, downloaded, usedTime)
|
||||||
if len(speedArr) > 6 {
|
}
|
||||||
speedArr = speedArr[1:]
|
|
||||||
|
func (t *Task) updateUploadSpeed(downloaded int64, usedTime float64) int64 {
|
||||||
|
return calcSpeed(&t.uploadSpeedArr, downloaded, usedTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calcSpeed(speedArr *[]int64, downloaded int64, usedTime float64) int64 {
|
||||||
|
*speedArr = append(*speedArr, downloaded)
|
||||||
|
// Record last 5 seconds of download speed to calculate the average speed
|
||||||
|
if len(*speedArr) > int(5.0/usedTime) {
|
||||||
|
*speedArr = (*speedArr)[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
for _, v := range speedArr {
|
for _, v := range *speedArr {
|
||||||
total += v
|
total += v
|
||||||
}
|
}
|
||||||
return int64(float64(total) / float64(len(speedArr)) / usedTime)
|
|
||||||
|
return int64(float64(total) / float64(len(*speedArr)) / usedTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
type TaskFilter struct {
|
type TaskFilter struct {
|
||||||
@@ -126,10 +137,10 @@ type DownloaderConfig struct {
|
|||||||
Controller *controller.Controller
|
Controller *controller.Controller
|
||||||
FetchManagers []fetcher.FetcherManager
|
FetchManagers []fetcher.FetcherManager
|
||||||
|
|
||||||
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
|
RefreshInterval int // RefreshInterval time duration to refresh task progress(ms)
|
||||||
Storage Storage
|
Storage Storage
|
||||||
StorageDir string
|
StorageDir string
|
||||||
DownloadDirWhiteList []string `json:"downloadDirWhiteList"`
|
WhiteDownloadDirs []string
|
||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ type OptsExtra struct {
|
|||||||
|
|
||||||
// Stats for download
|
// Stats for download
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
// http stats
|
Connections []*StatsConnection `json:"connections"`
|
||||||
// health indicators of http
|
}
|
||||||
|
|
||||||
|
type StatsConnection struct {
|
||||||
|
Downloaded int64 `json:"downloaded"`
|
||||||
|
Completed bool `json:"completed"`
|
||||||
|
Failed bool `json:"failed"`
|
||||||
|
RetryTimes int `json:"retryTimes"`
|
||||||
}
|
}
|
||||||
+4
-2
@@ -58,13 +58,13 @@ func CreateTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
|
||||||
var req model.CreateTaskBatch
|
var req base.CreateTaskBatch
|
||||||
if ReadJson(r, w, &req) {
|
if ReadJson(r, w, &req) {
|
||||||
if len(req.Reqs) == 0 {
|
if len(req.Reqs) == 0 {
|
||||||
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
|
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
taskIds, err := Downloader.CreateDirectBatch(req.Reqs, req.Opt)
|
taskIds, err := Downloader.CreateDirectBatch(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
return
|
return
|
||||||
@@ -309,6 +309,8 @@ func DoProxy(w http.ResponseWriter, r *http.Request) {
|
|||||||
r.RequestURI = ""
|
r.RequestURI = ""
|
||||||
r.URL = targetUrl
|
r.URL = targetUrl
|
||||||
r.Host = targetUrl.Host
|
r.Host = targetUrl.Host
|
||||||
|
r.Header.Del("Authorization")
|
||||||
|
r.Header.Del("X-Target-Uri")
|
||||||
resp, err := http.DefaultClient.Do(r)
|
resp, err := http.DefaultClient.Do(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, err.Error())
|
writeError(w, err.Error())
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type gzipResponseWriter struct {
|
||||||
|
io.Writer
|
||||||
|
http.ResponseWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g gzipResponseWriter) Write(b []byte) (int, error) {
|
||||||
|
return g.Writer.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func gzipMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Encoding", "gzip")
|
||||||
|
w.Header().Add("Vary", "Accept-Encoding")
|
||||||
|
|
||||||
|
gz := gzip.NewWriter(w)
|
||||||
|
defer gz.Close()
|
||||||
|
|
||||||
|
next.ServeHTTP(gzipResponseWriter{Writer: gz, ResponseWriter: w}, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
)
|
)
|
||||||
@@ -25,9 +24,9 @@ type StartConfig struct {
|
|||||||
|
|
||||||
ProductionMode bool
|
ProductionMode bool
|
||||||
|
|
||||||
WebEnable bool
|
WebEnable bool
|
||||||
WebFS fs.FS
|
WebFS fs.FS
|
||||||
WebBasicAuth *WebBasicAuth
|
WebAuth *WebAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *StartConfig) Init() *StartConfig {
|
func (cfg *StartConfig) Init() *StartConfig {
|
||||||
@@ -49,13 +48,7 @@ func (cfg *StartConfig) Init() *StartConfig {
|
|||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebBasicAuth struct {
|
type WebAuth struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authorization returns the value of the Authorization header to be used in HTTP requests.
|
|
||||||
func (cfg *WebBasicAuth) Authorization() string {
|
|
||||||
userId := cfg.Username + ":" + cfg.Password
|
|
||||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userId))
|
|
||||||
}
|
|
||||||
@@ -7,8 +7,3 @@ type CreateTask struct {
|
|||||||
Req *base.Request `json:"req"`
|
Req *base.Request `json:"req"`
|
||||||
Opt *base.Options `json:"opt"`
|
Opt *base.Options `json:"opt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateTaskBatch struct {
|
|
||||||
Reqs []*base.Request `json:"reqs"`
|
|
||||||
Opt *base.Options `json:"opt"`
|
|
||||||
}
|
|
||||||
+203
-12
@@ -2,6 +2,10 @@ package rest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download"
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
@@ -10,17 +14,22 @@ import (
|
|||||||
"github.com/gorilla/handlers"
|
"github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
srv *http.Server
|
srv *http.Server
|
||||||
runningPort int
|
runningPort int
|
||||||
|
aesKey []byte
|
||||||
|
|
||||||
Downloader *download.Downloader
|
Downloader *download.Downloader
|
||||||
)
|
)
|
||||||
@@ -74,8 +83,9 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
startCfg.Init()
|
startCfg.Init()
|
||||||
|
|
||||||
downloadCfg := &download.DownloaderConfig{
|
downloadCfg := &download.DownloaderConfig{
|
||||||
ProductionMode: startCfg.ProductionMode,
|
ProductionMode: startCfg.ProductionMode,
|
||||||
RefreshInterval: startCfg.RefreshInterval,
|
RefreshInterval: startCfg.RefreshInterval,
|
||||||
|
WhiteDownloadDirs: startCfg.WhiteDownloadDirs,
|
||||||
}
|
}
|
||||||
if startCfg.Storage == model.StorageBolt {
|
if startCfg.Storage == model.StorageBolt {
|
||||||
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
|
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
|
||||||
@@ -93,6 +103,13 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
util.SafeRemove(startCfg.Address)
|
util.SafeRemove(startCfg.Address)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if startCfg.WebEnable {
|
||||||
|
aesKey = make([]byte, 32)
|
||||||
|
if _, err := rand.Read(aesKey); err != nil {
|
||||||
|
return nil, nil, errors.Wrap(err, "generate aes key failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
listener, err := net.Listen(startCfg.Network, startCfg.Address)
|
listener, err := net.Listen(startCfg.Network, startCfg.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -123,27 +140,94 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
|
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
|
||||||
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
|
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
|
||||||
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
|
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
|
||||||
|
|
||||||
|
enableApiToken := startCfg.ApiToken != ""
|
||||||
|
enableBasicAuth := startCfg.WebEnable && startCfg.WebAuth != nil
|
||||||
if startCfg.WebEnable {
|
if startCfg.WebEnable {
|
||||||
|
if enableBasicAuth {
|
||||||
|
r.Methods(http.MethodPost).Path("/api/web/login").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var loginReq model.WebAuth
|
||||||
|
if ReadJson(r, w, &loginReq) {
|
||||||
|
if loginReq.Username == startCfg.WebAuth.Username && loginReq.Password == startCfg.WebAuth.Password {
|
||||||
|
// Generate a login token, Username:Password:Timestamp
|
||||||
|
timestamp := time.Now().Unix()
|
||||||
|
tokenData := fmt.Sprintf("%s:%s:%d", loginReq.Username, loginReq.Password, timestamp)
|
||||||
|
token, err := aesEncrypt(aesKey, []byte(tokenData))
|
||||||
|
if err != nil {
|
||||||
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteJson(w, model.NewOkResult(token))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||||
|
})
|
||||||
|
}
|
||||||
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
|
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
|
||||||
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
|
||||||
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
|
r.PathPrefix("/").Handler(gzipMiddleware(http.FileServer(newEmbedCacheFileSystem(http.FS(startCfg.WebFS)))))
|
||||||
}
|
}
|
||||||
|
if enableApiToken || enableBasicAuth {
|
||||||
|
writeUnauthorized := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
||||||
|
}
|
||||||
|
|
||||||
if startCfg.ApiToken != "" || (startCfg.WebEnable && startCfg.WebBasicAuth != nil) {
|
|
||||||
r.Use(func(h http.Handler) http.Handler {
|
r.Use(func(h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if startCfg.ApiToken != "" && r.Header.Get("X-Api-Token") == startCfg.ApiToken {
|
if enableApiToken {
|
||||||
h.ServeHTTP(w, r)
|
apiTokenHeader := r.Header["X-Api-Token"]
|
||||||
return
|
// If api token header is set, only check api token ignore basic auth
|
||||||
|
if len(apiTokenHeader) > 0 {
|
||||||
|
if apiTokenHeader[0] == startCfg.ApiToken {
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
|
|
||||||
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
|
if enableBasicAuth {
|
||||||
|
if r.URL.Path == "/api/web/login" {
|
||||||
h.ServeHTTP(w, r)
|
h.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
|
|
||||||
|
token := r.Header.Get("Authorization")
|
||||||
|
if token == "" {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token = strings.TrimPrefix(token, "Bearer ")
|
||||||
|
tokenData, err := aesDecrypt(aesKey, token)
|
||||||
|
if err != nil {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(string(tokenData), ":", 3)
|
||||||
|
username := parts[0]
|
||||||
|
password := parts[1]
|
||||||
|
timestamp, _ := strconv.Atoi(parts[2])
|
||||||
|
|
||||||
|
if username != startCfg.WebAuth.Username || password != startCfg.WebAuth.Password {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the token is expired (7 days)
|
||||||
|
if time.Now().Unix()-int64(timestamp) > 7*24*3600 {
|
||||||
|
writeUnauthorized(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
|
writeUnauthorized(w, r)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -163,9 +247,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
|
|||||||
})
|
})
|
||||||
|
|
||||||
srv = &http.Server{Handler: handlers.CORS(
|
srv = &http.Server{Handler: handlers.CORS(
|
||||||
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
|
handlers.AllowedHeaders([]string{"Content-Type", "Authorization", "X-Api-Token", "X-Target-Uri"}),
|
||||||
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
|
||||||
handlers.AllowedOrigins([]string{"*"}),
|
handlers.AllowedOrigins([]string{"*"}),
|
||||||
|
handlers.AllowCredentials(),
|
||||||
)(r)}
|
)(r)}
|
||||||
return srv, listener, nil
|
return srv, listener, nil
|
||||||
}
|
}
|
||||||
@@ -222,6 +307,68 @@ func (e *extensionFileSystem) Open(name string) (http.File, error) {
|
|||||||
return os.Open(filepath.Join(extensionPath, path))
|
return os.Open(filepath.Join(extensionPath, path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type embedCacheFileSystem struct {
|
||||||
|
fs http.FileSystem
|
||||||
|
lastModTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEmbedCacheFileSystem(fs http.FileSystem) *embedCacheFileSystem {
|
||||||
|
efs := &embedCacheFileSystem{
|
||||||
|
fs: fs,
|
||||||
|
lastModTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return efs
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := os.Stat(exe)
|
||||||
|
if err != nil {
|
||||||
|
return efs
|
||||||
|
}
|
||||||
|
|
||||||
|
efs.lastModTime = fi.ModTime()
|
||||||
|
return efs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embedCacheFileSystem) Open(name string) (http.File, error) {
|
||||||
|
file, err := e.fs.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &embedFile{
|
||||||
|
File: file,
|
||||||
|
lastModTime: e.lastModTime,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type embedFile struct {
|
||||||
|
http.File
|
||||||
|
lastModTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type embedFileInfo struct {
|
||||||
|
fs.FileInfo
|
||||||
|
lastModTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embedFileInfo) ModTime() time.Time {
|
||||||
|
return e.lastModTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embedFile) Stat() (fs.FileInfo, error) {
|
||||||
|
fi, err := e.File.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &embedFileInfo{
|
||||||
|
FileInfo: fi,
|
||||||
|
lastModTime: e.lastModTime,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
|
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
|
||||||
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
|
||||||
WriteJson(w, model.NewErrorResult(err.Error()))
|
WriteJson(w, model.NewErrorResult(err.Error()))
|
||||||
@@ -239,3 +386,47 @@ func WriteStatusJson(w http.ResponseWriter, statusCode int, v any) {
|
|||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
json.NewEncoder(w).Encode(v)
|
json.NewEncoder(w).Encode(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func aesEncrypt(key, data []byte) (string, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherText := gcm.Seal(nonce, nonce, data, nil)
|
||||||
|
return base64.StdEncoding.EncodeToString(cipherText), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func aesDecrypt(key []byte, encryptedData string) ([]byte, error) {
|
||||||
|
cipherText, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cipherText) < gcm.NonceSize() {
|
||||||
|
return nil, errors.New("ciphertext too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, cipherText := cipherText[:gcm.NonceSize()], cipherText[gcm.NonceSize():]
|
||||||
|
return gcm.Open(nil, nonce, cipherText, nil)
|
||||||
|
}
|
||||||
+192
-7
@@ -14,7 +14,9 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -139,11 +141,13 @@ func TestCreateDirectTask(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateDirectTaskBatch(t *testing.T) {
|
func TestCreateDirectTaskBatch(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
reqs := make([]*base.Request, 0)
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
reqs = append(reqs, createReq.Req)
|
reqs = append(reqs, &base.CreateTaskBatchItem{
|
||||||
|
Req: createReq.Req,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
|
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
|
||||||
Reqs: reqs,
|
Reqs: reqs,
|
||||||
})
|
})
|
||||||
if len(taskIds) != len(reqs) {
|
if len(taskIds) != len(reqs) {
|
||||||
@@ -152,6 +156,45 @@ func TestCreateDirectTaskBatch(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateDirectTaskBatchWithOpt(t *testing.T) {
|
||||||
|
doTest(func() {
|
||||||
|
reqs := make([]*base.CreateTaskBatchItem, 0)
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
item := &base.CreateTaskBatchItem{
|
||||||
|
Req: createReq.Req,
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
item.Opts = &base.Options{
|
||||||
|
Name: "spe_opt.data",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reqs = append(reqs, item)
|
||||||
|
}
|
||||||
|
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
|
||||||
|
Reqs: reqs,
|
||||||
|
Opts: &base.Options{
|
||||||
|
Name: "default_opt.data",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if len(taskIds) != len(reqs) {
|
||||||
|
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", len(taskIds), len(reqs))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, taskId := range taskIds {
|
||||||
|
task := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
|
||||||
|
if i == 0 {
|
||||||
|
if !strings.Contains(task.Name(), "spe_opt") {
|
||||||
|
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "spe_opt.data")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !strings.Contains(task.Name(), "default_opt") {
|
||||||
|
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "default_opt.data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateDirectTaskWithResoled(t *testing.T) {
|
func TestCreateDirectTaskWithResoled(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@@ -478,6 +521,66 @@ func TestFsExtensionFail(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWebFsEnhance(t *testing.T) {
|
||||||
|
indexHtml := `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>index</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>index</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
webDistPath := "dist"
|
||||||
|
os.MkdirAll("dist", os.ModePerm)
|
||||||
|
if err := os.WriteFile(filepath.Join(webDistPath, "index.html"), []byte(indexHtml), os.ModePerm); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(webDistPath)
|
||||||
|
|
||||||
|
doTest0(func(cfg *model.StartConfig) {
|
||||||
|
cfg.WebFS = os.DirFS(webDistPath)
|
||||||
|
}, func() {
|
||||||
|
// First request no cache
|
||||||
|
code, header, _ := doHttpRequest1(http.MethodGet, "/index.html", map[string]string{
|
||||||
|
"Accept-Encoding": "gzip",
|
||||||
|
}, nil)
|
||||||
|
if code != http.StatusOK {
|
||||||
|
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusOK)
|
||||||
|
}
|
||||||
|
// Check header last-modified
|
||||||
|
if _, ok := header["Last-Modified"]; !ok {
|
||||||
|
t.Errorf("TestWebFsEnhance() missing key = %v", "Last-Modified")
|
||||||
|
}
|
||||||
|
// Check gzip compress
|
||||||
|
if _, ok := header["Content-Encoding"]; !ok || header["Content-Encoding"] != "gzip" {
|
||||||
|
t.Errorf("TestWebFsEnhance() no gzip compress")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request with If-Modified-Since
|
||||||
|
ifModifiedSince := header["Last-Modified"]
|
||||||
|
code, _, _ = doHttpRequest1(http.MethodGet, "/index.html", map[string]string{
|
||||||
|
"If-Modified-Since": ifModifiedSince,
|
||||||
|
}, nil)
|
||||||
|
if code != http.StatusNotModified {
|
||||||
|
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusNotModified)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request with un gzip
|
||||||
|
code, header, _ = doHttpRequest1(http.MethodGet, "/index.html?t=123", nil, nil)
|
||||||
|
if code != http.StatusOK {
|
||||||
|
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusOK)
|
||||||
|
}
|
||||||
|
// Check no gzip compress
|
||||||
|
if _, ok := header["Content-Encoding"]; ok && header["Content-Encoding"] == "gzip" {
|
||||||
|
t.Errorf("TestWebFsEnhance() has gzip compress")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestDoProxy(t *testing.T) {
|
func TestDoProxy(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
|
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
|
||||||
@@ -534,7 +637,7 @@ func TestAuthorization(t *testing.T) {
|
|||||||
cfg.Init()
|
cfg.Init()
|
||||||
cfg.ApiToken = "123456"
|
cfg.ApiToken = "123456"
|
||||||
cfg.WebEnable = true
|
cfg.WebEnable = true
|
||||||
cfg.WebBasicAuth = &model.WebBasicAuth{
|
cfg.WebAuth = &model.WebAuth{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
Password: "123456",
|
Password: "123456",
|
||||||
}
|
}
|
||||||
@@ -546,14 +649,61 @@ func TestAuthorization(t *testing.T) {
|
|||||||
Stop()
|
Stop()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
status, _ := doHttpRequest0(http.MethodPost, "/api/web/login", nil, &model.WebAuth{
|
||||||
|
Username: "xxx",
|
||||||
|
Password: "xxx",
|
||||||
|
})
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := httpRequestCheckOk[string](http.MethodPost, "/api/web/login", cfg.WebAuth)
|
||||||
|
authToken := fmt.Sprintf("Bearer %s", token)
|
||||||
|
authHeaders := map[string]string{
|
||||||
|
"Authorization": authToken,
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
|
||||||
if status != http.StatusUnauthorized {
|
if status != http.StatusUnauthorized {
|
||||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
}
|
}
|
||||||
|
|
||||||
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
"Authorization": cfg.WebBasicAuth.Authorization(),
|
"Authorization": "xxx",
|
||||||
}, nil)
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": "xxx",
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildToken := func(username, password string, ts int64) string {
|
||||||
|
token, _ := aesEncrypt(aesKey, []byte(fmt.Sprintf("%s:%s:%d", username, password, ts)))
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeToken := buildToken("fake", "fake", time.Now().Unix())
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": fmt.Sprintf("Bearer %s", fakeToken),
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
expireToken := buildToken(cfg.WebAuth.Username, cfg.WebAuth.Password, time.Now().Add(-time.Hour*8*24).Unix())
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": fmt.Sprintf("Bearer %s", expireToken),
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", authHeaders, nil)
|
||||||
if status != http.StatusOK {
|
if status != http.StatusOK {
|
||||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||||
}
|
}
|
||||||
@@ -564,15 +714,38 @@ func TestAuthorization(t *testing.T) {
|
|||||||
if status != http.StatusOK {
|
if status != http.StatusOK {
|
||||||
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": authToken,
|
||||||
|
"X-Api-Token": cfg.ApiToken,
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusOK {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
|
||||||
|
"Authorization": authToken,
|
||||||
|
"X-Api-Token": "",
|
||||||
|
}, nil)
|
||||||
|
if status != http.StatusUnauthorized {
|
||||||
|
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func doTest(handler func()) {
|
func doTest(handler func()) {
|
||||||
|
doTest0(nil, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func doTest0(onStart func(cfg *model.StartConfig), handler func()) {
|
||||||
testFunc := func(storage model.Storage) {
|
testFunc := func(storage model.Storage) {
|
||||||
var cfg = &model.StartConfig{}
|
var cfg = &model.StartConfig{}
|
||||||
cfg.Init()
|
cfg.Init()
|
||||||
cfg.Storage = storage
|
cfg.Storage = storage
|
||||||
cfg.StorageDir = ".test_storage"
|
cfg.StorageDir = ".test_storage"
|
||||||
cfg.WebEnable = true
|
cfg.WebEnable = true
|
||||||
|
if onStart != nil {
|
||||||
|
onStart(cfg)
|
||||||
|
}
|
||||||
fileListener := doStart(cfg)
|
fileListener := doStart(cfg)
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := fileListener.Close(); err != nil {
|
if err := fileListener.Close(); err != nil {
|
||||||
@@ -582,6 +755,7 @@ func doTest(handler func()) {
|
|||||||
Downloader.Clear()
|
Downloader.Clear()
|
||||||
}()
|
}()
|
||||||
defer func() {
|
defer func() {
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
Downloader.Pause(nil)
|
Downloader.Pause(nil)
|
||||||
Downloader.Delete(nil, true)
|
Downloader.Delete(nil, true)
|
||||||
os.RemoveAll(cfg.StorageDir)
|
os.RemoveAll(cfg.StorageDir)
|
||||||
@@ -603,6 +777,11 @@ func doStart(cfg *model.StartConfig) net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) {
|
func doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) {
|
||||||
|
r1, _, r3 := doHttpRequest1(method, path, headers, body)
|
||||||
|
return r1, r3
|
||||||
|
}
|
||||||
|
|
||||||
|
func doHttpRequest1(method string, path string, headers map[string]string, body any) (int, map[string]string, []byte) {
|
||||||
var reader io.Reader
|
var reader io.Reader
|
||||||
if body != nil {
|
if body != nil {
|
||||||
buf, _ := json.Marshal(body)
|
buf, _ := json.Marshal(body)
|
||||||
@@ -623,12 +802,18 @@ func doHttpRequest0(method string, path string, headers map[string]string, body
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
respHeader := make(map[string]string)
|
||||||
|
for k, vv := range response.Header {
|
||||||
|
respHeader[k] = vv[0]
|
||||||
|
}
|
||||||
|
|
||||||
respBody, err := io.ReadAll(response.Body)
|
respBody, err := io.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.StatusCode, respBody
|
return response.StatusCode, respHeader, respBody
|
||||||
}
|
}
|
||||||
|
|
||||||
func doHttpRequest[T any](method string, path string, headers map[string]string, body any) (int, *model.Result[T]) {
|
func doHttpRequest[T any](method string, path string, headers map[string]string, body any) (int, *model.Result[T]) {
|
||||||
|
|||||||
@@ -69,5 +69,7 @@ windows/flutter/generated_plugins.cmake
|
|||||||
database.hive
|
database.hive
|
||||||
database.lock
|
database.lock
|
||||||
|
|
||||||
assets/host/host
|
assets/exec/host
|
||||||
assets/host/host.exe
|
assets/exec/host.exe
|
||||||
|
assets/exec/updater
|
||||||
|
assets/exec/updater.exe
|
||||||
File renamed without changes.
+68
-32
@@ -3,19 +3,24 @@ import 'dart:io';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio/io.dart';
|
import 'package:dio/io.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:gopeed/api/model/extension.dart';
|
import 'package:get/get.dart' as getx;
|
||||||
import 'package:gopeed/api/model/install_extension.dart';
|
|
||||||
import 'package:gopeed/api/model/switch_extension.dart';
|
|
||||||
|
|
||||||
|
import '../app/routes/app_pages.dart';
|
||||||
|
import '../database/database.dart';
|
||||||
import '../util/util.dart';
|
import '../util/util.dart';
|
||||||
import 'model/create_task.dart';
|
import 'model/create_task.dart';
|
||||||
|
import 'model/create_task_batch.dart';
|
||||||
import 'model/downloader_config.dart';
|
import 'model/downloader_config.dart';
|
||||||
|
import 'model/extension.dart';
|
||||||
|
import 'model/install_extension.dart';
|
||||||
|
import 'model/login.dart';
|
||||||
import 'model/request.dart';
|
import 'model/request.dart';
|
||||||
import 'model/resolve_result.dart';
|
import 'model/resolve_result.dart';
|
||||||
import 'model/result.dart';
|
import 'model/result.dart';
|
||||||
|
import 'model/switch_extension.dart';
|
||||||
import 'model/task.dart';
|
import 'model/task.dart';
|
||||||
import 'model/update_extension_settings.dart';
|
|
||||||
import 'model/update_check_extension_resp.dart';
|
import 'model/update_check_extension_resp.dart';
|
||||||
|
import 'model/update_extension_settings.dart';
|
||||||
|
|
||||||
class _Client {
|
class _Client {
|
||||||
static _Client? _instance;
|
static _Client? _instance;
|
||||||
@@ -29,12 +34,12 @@ class _Client {
|
|||||||
_instance = _Client._internal();
|
_instance = _Client._internal();
|
||||||
var dio = Dio();
|
var dio = Dio();
|
||||||
final isUnixSocket = network == 'unix';
|
final isUnixSocket = network == 'unix';
|
||||||
var baseUrl = 'http://127.0.0.1';
|
var baseUrl = 'http://127.0.0.1/';
|
||||||
if (!isUnixSocket) {
|
if (!isUnixSocket) {
|
||||||
if (Util.isWeb()) {
|
if (Util.isWeb()) {
|
||||||
baseUrl = kDebugMode ? 'http://127.0.0.1:9999' : '';
|
baseUrl = kDebugMode ? 'http://127.0.0.1:9999/' : '';
|
||||||
} else {
|
} else {
|
||||||
baseUrl = 'http://$address';
|
baseUrl = 'http://$address/';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dio.options.baseUrl = baseUrl;
|
dio.options.baseUrl = baseUrl;
|
||||||
@@ -42,12 +47,27 @@ class _Client {
|
|||||||
dio.options.sendTimeout = const Duration(seconds: 5);
|
dio.options.sendTimeout = const Duration(seconds: 5);
|
||||||
dio.options.connectTimeout = const Duration(seconds: 5);
|
dio.options.connectTimeout = const Duration(seconds: 5);
|
||||||
dio.options.receiveTimeout = const Duration(seconds: 60);
|
dio.options.receiveTimeout = const Duration(seconds: 60);
|
||||||
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
|
dio.interceptors.add(InterceptorsWrapper(
|
||||||
if (apiToken.isNotEmpty) {
|
onRequest: (options, handler) {
|
||||||
options.headers['X-Api-Token'] = apiToken;
|
if (apiToken.isNotEmpty) {
|
||||||
}
|
options.headers['X-Api-Token'] = apiToken;
|
||||||
handler.next(options);
|
}
|
||||||
}));
|
if (Util.isWeb()) {
|
||||||
|
final token = Database.instance.getWebToken();
|
||||||
|
if (token != null) {
|
||||||
|
options.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.next(options);
|
||||||
|
},
|
||||||
|
onError: (error, handler) {
|
||||||
|
// Only web version has a login page
|
||||||
|
if (Util.isWeb() && error.response?.statusCode == 401) {
|
||||||
|
getx.Get.rootDelegate.offAndToNamed(Routes.LOGIN);
|
||||||
|
}
|
||||||
|
handler.next(error);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
_instance!.dio = dio;
|
_instance!.dio = dio;
|
||||||
if (isUnixSocket) {
|
if (isUnixSocket) {
|
||||||
@@ -95,7 +115,8 @@ Future<T> _parse<T>(
|
|||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.type == DioExceptionType.sendTimeout ||
|
if (e.type == DioExceptionType.sendTimeout ||
|
||||||
e.type == DioExceptionType.receiveTimeout ||
|
e.type == DioExceptionType.receiveTimeout ||
|
||||||
e.type == DioExceptionType.connectionTimeout) {
|
e.type == DioExceptionType.connectionTimeout ||
|
||||||
|
e.type == DioExceptionType.connectionError) {
|
||||||
throw TimeoutException("request timeout");
|
throw TimeoutException("request timeout");
|
||||||
}
|
}
|
||||||
throw Exception(Result(code: 1000, msg: e.message));
|
throw Exception(Result(code: 1000, msg: e.message));
|
||||||
@@ -104,16 +125,22 @@ Future<T> _parse<T>(
|
|||||||
|
|
||||||
Future<ResolveResult> resolve(Request request) async {
|
Future<ResolveResult> resolve(Request request) async {
|
||||||
return _parse<ResolveResult>(
|
return _parse<ResolveResult>(
|
||||||
() => _client.dio.post("/api/v1/resolve", data: request),
|
() => _client.dio.post("api/v1/resolve", data: request),
|
||||||
(data) => ResolveResult.fromJson(data));
|
(data) => ResolveResult.fromJson(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> createTask(CreateTask createTask) async {
|
Future<String> createTask(CreateTask createTask) async {
|
||||||
return _parse<String>(
|
return _parse<String>(
|
||||||
() => _client.dio.post("/api/v1/tasks", data: createTask),
|
() => _client.dio.post("api/v1/tasks", data: createTask),
|
||||||
(data) => data as String);
|
(data) => data as String);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<String>> createTaskBatch(CreateTaskBatch createTaskBatch) async {
|
||||||
|
return _parse<List<String>>(
|
||||||
|
() => _client.dio.post("api/v1/tasks/batch", data: createTaskBatch),
|
||||||
|
(data) => (data as List).map((e) => e as String).toList());
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<Task>> getTasks(List<Status> statuses) async {
|
Future<List<Task>> getTasks(List<Status> statuses) async {
|
||||||
return _parse<List<Task>>(
|
return _parse<List<Task>>(
|
||||||
() => _client.dio.get(
|
() => _client.dio.get(
|
||||||
@@ -122,16 +149,16 @@ Future<List<Task>> getTasks(List<Status> statuses) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pauseTask(String id) async {
|
Future<void> pauseTask(String id) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/pause"), null);
|
return _parse(() => _client.dio.put("api/v1/tasks/$id/pause"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> continueTask(String id) async {
|
Future<void> continueTask(String id) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
|
return _parse(() => _client.dio.put("api/v1/tasks/$id/continue"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pauseAllTasks(List<String>? ids) async {
|
Future<void> pauseAllTasks(List<String>? ids) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.put("/api/v1/tasks/pause", queryParameters: {
|
() => _client.dio.put("api/v1/tasks/pause", queryParameters: {
|
||||||
"id": ids,
|
"id": ids,
|
||||||
}),
|
}),
|
||||||
null);
|
null);
|
||||||
@@ -139,7 +166,7 @@ Future<void> pauseAllTasks(List<String>? ids) async {
|
|||||||
|
|
||||||
Future<void> continueAllTasks(List<String>? ids) async {
|
Future<void> continueAllTasks(List<String>? ids) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.put("/api/v1/tasks/continue", queryParameters: {
|
() => _client.dio.put("api/v1/tasks/continue", queryParameters: {
|
||||||
"id": ids,
|
"id": ids,
|
||||||
}),
|
}),
|
||||||
null);
|
null);
|
||||||
@@ -147,12 +174,12 @@ Future<void> continueAllTasks(List<String>? ids) async {
|
|||||||
|
|
||||||
Future<void> deleteTask(String id, bool force) async {
|
Future<void> deleteTask(String id, bool force) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
|
() => _client.dio.delete("api/v1/tasks/$id?force=$force"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteTasks(List<String>? ids, bool force) async {
|
Future<void> deleteTasks(List<String>? ids, bool force) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.delete("/api/v1/tasks", queryParameters: {
|
() => _client.dio.delete("api/v1/tasks", queryParameters: {
|
||||||
"id": ids,
|
"id": ids,
|
||||||
"force": force,
|
"force": force,
|
||||||
}),
|
}),
|
||||||
@@ -160,29 +187,29 @@ Future<void> deleteTasks(List<String>? ids, bool force) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<DownloaderConfig> getConfig() async {
|
Future<DownloaderConfig> getConfig() async {
|
||||||
return _parse(() => _client.dio.get("/api/v1/config"),
|
return _parse(() => _client.dio.get("api/v1/config"),
|
||||||
(data) => DownloaderConfig.fromJson(data));
|
(data) => DownloaderConfig.fromJson(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> putConfig(DownloaderConfig config) async {
|
Future<void> putConfig(DownloaderConfig config) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/config", data: config), null);
|
return _parse(() => _client.dio.put("api/v1/config", data: config), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> installExtension(InstallExtension installExtension) async {
|
Future<void> installExtension(InstallExtension installExtension) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.post("/api/v1/extensions", data: installExtension),
|
() => _client.dio.post("api/v1/extensions", data: installExtension),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Extension>> getExtensions() async {
|
Future<List<Extension>> getExtensions() async {
|
||||||
return _parse<List<Extension>>(() => _client.dio.get("/api/v1/extensions"),
|
return _parse<List<Extension>>(() => _client.dio.get("api/v1/extensions"),
|
||||||
(data) => (data as List).map((e) => Extension.fromJson(e)).toList());
|
(data) => (data as List).map((e) => Extension.fromJson(e)).toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateExtensionSettings(
|
Future<void> updateExtensionSettings(
|
||||||
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
String identity, UpdateExtensionSettings updateExtensionSettings) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.put("/api/v1/extensions/$identity/settings",
|
() => _client.dio.put("api/v1/extensions/$identity/settings",
|
||||||
data: updateExtensionSettings),
|
data: updateExtensionSettings),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
@@ -191,22 +218,27 @@ Future<void> switchExtension(
|
|||||||
String identity, SwitchExtension switchExtension) async {
|
String identity, SwitchExtension switchExtension) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio
|
() => _client.dio
|
||||||
.put("/api/v1/extensions/$identity/switch", data: switchExtension),
|
.put("api/v1/extensions/$identity/switch", data: switchExtension),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteExtension(String identity) async {
|
Future<void> deleteExtension(String identity) async {
|
||||||
return _parse(() => _client.dio.delete("/api/v1/extensions/$identity"), null);
|
return _parse(() => _client.dio.delete("api/v1/extensions/$identity"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity) async {
|
Future<UpdateCheckExtensionResp> upgradeCheckExtension(String identity) async {
|
||||||
return _parse(() => _client.dio.get("/api/v1/extensions/$identity/update"),
|
return _parse(() => _client.dio.get("api/v1/extensions/$identity/update"),
|
||||||
(data) => UpdateCheckExtensionResp.fromJson(data));
|
(data) => UpdateCheckExtensionResp.fromJson(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateExtension(String identity) async {
|
Future<void> updateExtension(String identity) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.post("/api/v1/extensions/$identity/update"), null);
|
() => _client.dio.post("api/v1/extensions/$identity/update"), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> login(LoginReq loginReq) async {
|
||||||
|
return _parse(() => _client.dio.post("api/web/login", data: loginReq),
|
||||||
|
(data) => data as String);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response<String>> proxyRequest<T>(String uri,
|
Future<Response<String>> proxyRequest<T>(String uri,
|
||||||
@@ -223,5 +255,9 @@ Future<Response<String>> proxyRequest<T>(String uri,
|
|||||||
}
|
}
|
||||||
|
|
||||||
String join(String path) {
|
String join(String path) {
|
||||||
return "${_client.dio.options.baseUrl}/${Util.cleanPath(path)}";
|
final baseUrl = _client.dio.options.baseUrl;
|
||||||
|
final cleanBaseUrl = baseUrl.endsWith('/')
|
||||||
|
? baseUrl.substring(0, baseUrl.length - 1)
|
||||||
|
: baseUrl;
|
||||||
|
return "$cleanBaseUrl/${Util.cleanPath(path)}";
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
import 'options.dart';
|
||||||
|
import 'request.dart';
|
||||||
|
|
||||||
|
part 'create_task_batch.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable(explicitToJson: true)
|
||||||
|
class CreateTaskBatch {
|
||||||
|
List<CreateTaskBatchItem>? reqs;
|
||||||
|
Options? opt;
|
||||||
|
|
||||||
|
CreateTaskBatch({
|
||||||
|
this.reqs,
|
||||||
|
this.opt,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CreateTaskBatch.fromJson(
|
||||||
|
Map<String, dynamic> json,
|
||||||
|
) =>
|
||||||
|
_$CreateTaskBatchFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$CreateTaskBatchToJson(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CreateTaskBatchItem {
|
||||||
|
Request? req;
|
||||||
|
Options? opts;
|
||||||
|
|
||||||
|
CreateTaskBatchItem({
|
||||||
|
this.req,
|
||||||
|
this.opts,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CreateTaskBatchItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$CreateTaskBatchItemFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$CreateTaskBatchItemToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'create_task_batch.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
CreateTaskBatch _$CreateTaskBatchFromJson(Map<String, dynamic> json) =>
|
||||||
|
CreateTaskBatch(
|
||||||
|
reqs: (json['reqs'] as List<dynamic>?)
|
||||||
|
?.map((e) => CreateTaskBatchItem.fromJson(e as Map<String, dynamic>))
|
||||||
|
.toList(),
|
||||||
|
opt: json['opt'] == null
|
||||||
|
? null
|
||||||
|
: Options.fromJson(json['opt'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CreateTaskBatchToJson(CreateTaskBatch instance) {
|
||||||
|
final val = <String, dynamic>{};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('reqs', instance.reqs?.map((e) => e.toJson()).toList());
|
||||||
|
writeNotNull('opt', instance.opt?.toJson());
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateTaskBatchItem _$CreateTaskBatchItemFromJson(Map<String, dynamic> json) =>
|
||||||
|
CreateTaskBatchItem(
|
||||||
|
req: json['req'] == null
|
||||||
|
? null
|
||||||
|
: Request.fromJson(json['req'] as Map<String, dynamic>),
|
||||||
|
opts: json['opts'] == null
|
||||||
|
? null
|
||||||
|
: Options.fromJson(json['opts'] as Map<String, dynamic>),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$CreateTaskBatchItemToJson(CreateTaskBatchItem instance) {
|
||||||
|
final val = <String, dynamic>{};
|
||||||
|
|
||||||
|
void writeNotNull(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
val[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeNotNull('req', instance.req);
|
||||||
|
writeNotNull('opts', instance.opts);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'login.g.dart';
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class LoginReq {
|
||||||
|
String username;
|
||||||
|
String password;
|
||||||
|
|
||||||
|
LoginReq({
|
||||||
|
required this.username,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory LoginReq.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$LoginReqFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$LoginReqToJson(this);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'login.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
LoginReq _$LoginReqFromJson(Map<String, dynamic> json) => LoginReq(
|
||||||
|
username: json['username'] as String,
|
||||||
|
password: json['password'] as String,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$LoginReqToJson(LoginReq instance) => <String, dynamic>{
|
||||||
|
'username': instance.username,
|
||||||
|
'password': instance.password,
|
||||||
|
};
|
||||||
@@ -24,9 +24,11 @@ import '../../../../database/database.dart';
|
|||||||
import '../../../../database/entity.dart';
|
import '../../../../database/entity.dart';
|
||||||
import '../../../../i18n/message.dart';
|
import '../../../../i18n/message.dart';
|
||||||
import '../../../../main.dart';
|
import '../../../../main.dart';
|
||||||
|
import '../../../../util/github_mirror.dart';
|
||||||
import '../../../../util/locale_manager.dart';
|
import '../../../../util/locale_manager.dart';
|
||||||
import '../../../../util/log_util.dart';
|
import '../../../../util/log_util.dart';
|
||||||
import '../../../../util/package_info.dart';
|
import '../../../../util/package_info.dart';
|
||||||
|
import '../../../../util/updater.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
import '../../create/dto/create_router_params.dart';
|
import '../../create/dto/create_router_params.dart';
|
||||||
@@ -35,47 +37,22 @@ import '../../redirect/views/redirect_view.dart';
|
|||||||
const unixSocketPath = 'gopeed.sock';
|
const unixSocketPath = 'gopeed.sock';
|
||||||
|
|
||||||
const allTrackerSubscribeUrls = [
|
const allTrackerSubscribeUrls = [
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_http.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_http.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_https.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_https.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ip.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_udp.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_udp.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ws.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ws.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_best.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt',
|
||||||
'https://github.com/ngosang/trackerslist/raw/master/trackers_best_ip.txt',
|
'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best_ip.txt',
|
||||||
'https://github.com/XIU2/TrackersListCollection/raw/master/all.txt',
|
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/all.txt',
|
||||||
'https://github.com/XIU2/TrackersListCollection/raw/master/best.txt',
|
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/best.txt',
|
||||||
'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt',
|
'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/http.txt',
|
||||||
];
|
];
|
||||||
const allTrackerCdns = [
|
final allTrackerSubscribeUrlCdns = {
|
||||||
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
for (var v in allTrackerSubscribeUrls)
|
||||||
["https://fastly.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
|
v: githubMirrorUrls(v, MirrorType.githubSource)
|
||||||
// ghproxy: https://ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
};
|
||||||
[
|
|
||||||
"https://ghproxy.com/https://raw.githubusercontent.com",
|
|
||||||
r".*github.com(/.*)/raw(/.*)"
|
|
||||||
],
|
|
||||||
// mirror.ghproxy.com: https://mirror.ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
|
||||||
[
|
|
||||||
"https://mirror.ghproxy.com/https://raw.githubusercontent.com",
|
|
||||||
r".*github.com(/.*)/raw(/.*)"
|
|
||||||
],
|
|
||||||
];
|
|
||||||
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
|
||||||
key: (v) => v as String,
|
|
||||||
value: (v) {
|
|
||||||
final ret = [v as String];
|
|
||||||
for (final cdn in allTrackerCdns) {
|
|
||||||
final reg = RegExp(cdn[1]);
|
|
||||||
final match = reg.firstMatch(v.toString());
|
|
||||||
var matchStr = "";
|
|
||||||
for (var i = 1; i <= match!.groupCount; i++) {
|
|
||||||
matchStr += match.group(i)!;
|
|
||||||
}
|
|
||||||
ret.add("${cdn[0]}$matchStr");
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
|
|
||||||
class AppController extends GetxController with WindowListener, TrayListener {
|
class AppController extends GetxController with WindowListener, TrayListener {
|
||||||
static StartConfig? _defaultStartConfig;
|
static StartConfig? _defaultStartConfig;
|
||||||
@@ -109,6 +86,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
|
|
||||||
_initLaunchAtStartup().onError((error, stackTrace) =>
|
_initLaunchAtStartup().onError((error, stackTrace) =>
|
||||||
logger.w("initLaunchAtStartup error", error, stackTrace));
|
logger.w("initLaunchAtStartup error", error, stackTrace));
|
||||||
|
|
||||||
|
_initCheckUpdate().onError((error, stackTrace) =>
|
||||||
|
logger.w("initCheckUpdate error", error, stackTrace));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -359,7 +339,8 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
uri.scheme == "https") {
|
uri.scheme == "https") {
|
||||||
path = uri.toString();
|
path = uri.toString();
|
||||||
} else if (uri.scheme == "file") {
|
} else if (uri.scheme == "file") {
|
||||||
path = Util.isWindows() ? uri.path.substring(1) : uri.path;
|
path =
|
||||||
|
Util.isWindows() ? Uri.decodeFull(uri.path.substring(1)) : uri.path;
|
||||||
} else {
|
} else {
|
||||||
path = (await toFile(uri.toString())).path;
|
path = (await toFile(uri.toString())).path;
|
||||||
}
|
}
|
||||||
@@ -524,6 +505,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
autoStartup.value = await launchAtStartup.isEnabled();
|
autoStartup.value = await launchAtStartup.isEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _initCheckUpdate() async {
|
||||||
|
final versionInfo = await checkUpdate();
|
||||||
|
if (versionInfo != null) {
|
||||||
|
await showUpdateDialog(Get.context!, versionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> saveConfig() async {
|
Future<void> saveConfig() async {
|
||||||
Database.instance.saveStartConfig(StartConfigEntity(
|
Database.instance.saveStartConfig(StartConfigEntity(
|
||||||
network: startConfig.value.network,
|
network: startConfig.value.network,
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:window_manager/window_manager.dart'; // Import the required packages
|
||||||
|
|
||||||
import '../../../../i18n/message.dart';
|
import '../../../../i18n/message.dart';
|
||||||
import '../../../../theme/theme.dart';
|
import '../../../../theme/theme.dart';
|
||||||
import '../../../../util/locale_manager.dart';
|
import '../../../../util/locale_manager.dart';
|
||||||
|
import '../../../../util/util.dart'; // Import the required packages
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
import '../controllers/app_controller.dart';
|
import '../controllers/app_controller.dart';
|
||||||
|
|
||||||
@@ -32,6 +34,19 @@ class AppView extends GetView<AppController> {
|
|||||||
],
|
],
|
||||||
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
|
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
|
||||||
getPages: AppPages.routes,
|
getPages: AppPages.routes,
|
||||||
|
|
||||||
|
// Add listening to theme changes, set the title bar color according to the current system theme.
|
||||||
|
builder: (context, child) {
|
||||||
|
// if platform is desktop
|
||||||
|
if (Util.isDesktop()){
|
||||||
|
// actual brightness of the UI
|
||||||
|
Brightness brightness = Theme.of(context).brightness;
|
||||||
|
// Set the title bar to use the actual brightness of the UI
|
||||||
|
windowManager.setBrightness(brightness);
|
||||||
|
}
|
||||||
|
return child!;
|
||||||
|
},
|
||||||
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:rounded_loading_button_plus/rounded_loading_button.dart';
|
|||||||
|
|
||||||
import '../../../../api/api.dart';
|
import '../../../../api/api.dart';
|
||||||
import '../../../../api/model/create_task.dart';
|
import '../../../../api/model/create_task.dart';
|
||||||
|
import '../../../../api/model/create_task_batch.dart';
|
||||||
import '../../../../api/model/options.dart';
|
import '../../../../api/model/options.dart';
|
||||||
import '../../../../api/model/request.dart';
|
import '../../../../api/model/request.dart';
|
||||||
import '../../../../api/model/resolve_result.dart';
|
import '../../../../api/model/resolve_result.dart';
|
||||||
@@ -869,18 +870,20 @@ class CreateView extends GetView<CreateController> {
|
|||||||
if (createFormKey.currentState!.validate()) {
|
if (createFormKey.currentState!.validate()) {
|
||||||
if (rr.id.isEmpty) {
|
if (rr.id.isEmpty) {
|
||||||
// from extension resolve result
|
// from extension resolve result
|
||||||
await Future.wait(
|
final reqs =
|
||||||
controller.selectedIndexes.map((index) {
|
controller.selectedIndexes.map((index) {
|
||||||
final file = rr.res.files[index];
|
final file = rr.res.files[index];
|
||||||
return createTask(CreateTask(
|
return CreateTaskBatchItem(
|
||||||
req: file.req!..proxy = parseProxy(),
|
req: file.req!..proxy = parseProxy(),
|
||||||
opt: Options(
|
opts: Options(
|
||||||
name: file.name,
|
name: file.name,
|
||||||
path: path.join(_pathController.text,
|
path: path.join(_pathController.text,
|
||||||
rr.res.name, file.path),
|
rr.res.name, file.path),
|
||||||
selectFiles: [],
|
selectFiles: [],
|
||||||
extra: optExtra)));
|
extra: optExtra));
|
||||||
}));
|
}).toList();
|
||||||
|
await createTaskBatch(
|
||||||
|
CreateTaskBatch(reqs: reqs));
|
||||||
} else {
|
} else {
|
||||||
await createTask(CreateTask(
|
await createTask(CreateTask(
|
||||||
rid: rr.id,
|
rid: rr.id,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import '../../../../api/model/extension.dart';
|
|||||||
import '../../../../api/model/install_extension.dart';
|
import '../../../../api/model/install_extension.dart';
|
||||||
import '../../../../api/model/switch_extension.dart';
|
import '../../../../api/model/switch_extension.dart';
|
||||||
import '../../../../api/model/update_extension_settings.dart';
|
import '../../../../api/model/update_extension_settings.dart';
|
||||||
|
import '../../../../database/database.dart';
|
||||||
import '../../../../util/message.dart';
|
import '../../../../util/message.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../views/icon_button_loading.dart';
|
import '../../../views/icon_button_loading.dart';
|
||||||
@@ -135,6 +136,10 @@ class ExtensionView extends GetView<ExtensionController> {
|
|||||||
'/fs/extensions/${extension.identity}/${extension.icon}'),
|
'/fs/extensions/${extension.identity}/${extension.icon}'),
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
|
headers: {
|
||||||
|
'Authorization':
|
||||||
|
'Bearer ${Database.instance.getWebToken()}'
|
||||||
|
},
|
||||||
)
|
)
|
||||||
: Image.file(
|
: Image.file(
|
||||||
extension.devMode
|
extension.devMode
|
||||||
|
|||||||
Loaded 50 of 80 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user